Compare commits

...
107 Commits
Author SHA1 Message Date
steve-chavez d6050c8615 Bump to version v7.0.1 2020-05-18 13:05:09 -05:00
steve-chavez 7ffac522e3 circleci: remove linux distro releases
Prefer the linux static binary for x64
2020-05-18 13:05:09 -05:00
steve-chavez 6c4208d9e7 cirrusci: add binary artifact 2020-05-18 13:05:09 -05:00
steve-chavez 98bf4d861d circleci: add static bin to release 2020-05-18 13:05:09 -05:00
Remo RechkemmerandGitHub bfcd289855 Build Docker image from Nix (#1522)
* Static docker image

* added tags for latest and with version

* add postgrest repository to docker image name

* set creation date on nix docker images
2020-05-16 11:35:32 -05:00
Remo RechkemmerandGitHub 7c0fbf9b3f Add static build with Nix (fixes #1295) (#1494)
* Include modifications to nixpkgs and static-haskell-nix as patches.

* use the patched static-haskell-nix version

* Add cachix postgrest
2020-05-12 13:31:37 -05:00
steve-chavez 6fae07241f Add FreeBSD CI 2020-05-08 15:11:44 -05:00
steve-chavez a4f687fdfd appveyor: specify windows image 2020-05-08 15:11:44 -05:00
steve-chavez b1a101c253 Update BACKERS.md 2020-05-04 12:17:52 -05:00
steve-chavez 10c363b588 Remove PUT restriction for all columns/single row
Fixes https://github.com/PostgREST/postgrest/issues/1452.

The single row restriction can be lifted because the
PUT will consider only the first object of the array.
2020-05-02 11:51:32 -05:00
steve-chavez 9a52632024 Fix Content-Profile not working for POST RPC
Accept-Profile was applied instead.

Fixes https://github.com/PostgREST/postgrest/issues/1508.
2020-05-02 11:51:32 -05:00
steve-chavez f57caf0987 Readd openapi-server-proxy-uri config option
This option accidentaly got lost during a merge.

Fixes https://github.com/PostgREST/postgrest/issues/1500
2020-05-02 11:51:32 -05:00
RemoandGitHub f02904a959 Simplify the DbStructure queries (#1478)
* remove unused parts of the DbStructure queries

* explain magic numbers in DbStructure queries

* simplify primary keys query
2020-04-29 13:01:46 -05:00
RemoandGitHub 8b41b71db7 More consistent loading of the test fixtures (#1506)
Remove db setup from the Haskell test suite and include it in the test bash scripts.
2020-04-29 12:22:15 -05:00
RemoandGitHub d4cf8e7abb Hook the Nix styling scripts into CI (#1504) 2020-04-29 10:44:11 -05:00
RemoandGitHub 9134171b95 Fix the cachix push logic in CircleCI
Before pushing to cachix, check if the CACHIX_SIGNING_KEY env var is available.
2020-04-28 14:04:06 -05:00
RemoandGitHub 328c3453f8 Add tips and tricks to using nix-shell (#1505) 2020-04-28 12:45:29 -05:00
Robert VollmertandSteve Chavez bb27eb57a9 Allow base64-bytestring 1.2. 2020-04-28 11:06:05 -05:00
RemoandGitHub dbc3aa28c4 Add scripts for linting and styling with all dependencies managed by Nix (#1501)
* Integrate the style and lint dependencies with Nix

* Add documentation on the nix styling scripts.
2020-04-27 15:04:50 -05:00
RemoandGitHub ad92c207f2 Run the spec test suite from the nix build job (#1503)
* Add caching to the tests run in CI with Nix
2020-04-27 13:39:57 -05:00
RemoandSteve Chavez dea6c5eb92 Remove ghc options from stack.yaml
They are redundant after being added to postgrest.cabal in #1489
2020-04-27 12:41:55 -05:00
RobertandGitHub 3baefa1d96 Relax dependencies to build with GHC 8.10 (base, swagger2) (#1502)
* Allow swagger 2.7.
* Allow base-4.14 (GHC 8.10).
2020-04-25 13:35:01 -05:00
RemoandGitHub c0546e0e46 Basic CI integration for Nix builds (#1495)
* Skip pushing to cachix for pull requests from forks.
* Build PostgREST first in the nix CI job in order to fail fast
2020-04-25 13:08:30 -05:00
RemoandGitHub d5f1d1b1ad Pin version 0.3 of protolude in nix (#1499) 2020-04-24 12:09:56 -05:00
RemoandGitHub 1c19bbde93 Run tests against several PostgreSQL versions using Nix (#1492)
* Show test output with postgrest-test-spec* in nix-shell

* Test output in color
2020-04-24 11:05:37 -05:00
Robert VollmertandSteve Chavez 3da5a2875e Update to protolude 0.3.0
The good part is that protolude 0.3.0 builds with GHC 8.10.

The bad part is that this change is a bit painful:
- the default `toS` has changed to no longer convert to and from ByteString
- similarly, `show` no longer outputs ByteString

The changes here are pretty much minimal to keep things compiling; I didn't
see a nice way to work with the new ConvertText class, even though `toUtf8`
seems like it might help if used besides `toS` at just the right spots.
2020-04-23 14:14:21 -05:00
RemoandSteve Chavez 2e6a094d48 Disable nix pure by default in stack.yaml 2020-04-23 12:48:04 -05:00
RemoandSteve Chavez 178c5d54d5 Integrate stack in the nix-shell environment 2020-04-22 14:30:02 -05:00
RemoandGitHub a7c396e464 Fix and improvements to with_tmp_db (#1488)
* Load fixtures in one psql session

* Add an option to keet the tmp directory and logs.
2020-04-22 11:57:40 -05:00
RemoandGitHub 24db4a1e25 Basics for building PostgREST with Nix (#1489)
* Add GHC options used with stack to postgrest.cabal

* Add a Nix derivation for just the executable.
2020-04-21 11:59:00 -05:00
RobertandGitHub ae77cf9a08 circleci: Build linters with memory-saving ghc-options. (#1490)
Previously, the linters would use the cached build artefacts from
building postgrest with -fno-spec-constr via stack.yaml. Potentially
passing it to the linter build explicitly will fix recent out-of-memory
issues.
2020-04-19 14:15:12 -05:00
Robert VollmertandSteve Chavez 4a0a37588f Fix hlint 2.2.11 bracketing suggestions 2020-04-16 17:47:52 -05:00
Robert VollmertandSteve Chavez 5838214910 Fix tests for explicit errors in aeson-1.4.7, and raise lower bound 2020-04-16 17:47:52 -05:00
Robert VollmertandSteve Chavez b609d8491e Update stack.yaml to lts-15.8 (GHC 8.8.3) 2020-04-16 17:47:52 -05:00
Robert VollmertandSteve Chavez 18538707ab Relax upper bounds for lens, network-uri, time.
This allows lens-4.19, network-uri-2.7 and time-0.10. According to
the respective changelogs, none of these should cause trouble.

lens-4.19 is required to build with GHC 8.10.
2020-04-16 13:36:26 -05:00
Robert VollmertandSteve Chavez e59c72cff3 circleci: Break out a plain build and cache job 2020-04-16 13:10:21 -05:00
Robert VollmertandSteve Chavez da573a1805 circleci: Dedicated cache key for style checks 2020-04-16 13:10:21 -05:00
Robert VollmertandSteve Chavez e24a7d005a circleci: Split style and hlint out into separate job 2020-04-16 13:10:21 -05:00
Robert VollmertandSteve Chavez 79399686db Remove unused imports (with newer protolude)
The changes here are from fixing stack build with LTS 15.8 (GHC 8.8.3)
to be warning-free. Most if not all come from the newer protolude
version, which adds some things like `unwords`.

So to make sure that we don't get unused import warnings with newer
stackage, this both raises the protolude lower bound and removes
these imports.
2020-04-16 11:10:24 -05:00
RemoandGitHub f6ce93f2c8 Add a script for running the test suite in a local temporary database. (#1476) 2020-04-14 12:34:50 -05:00
RemoandGitHub 8c35c9d711 Fix CI tests for Postgres 9.4 (#1482) 2020-04-14 11:00:43 -05:00
Wolfgang WaltherandGitHub be674eb41d Fix POST, PATCH, DELETE with ?select= and empty body or return=minimal (#1471)
* add tests for POST, PATCH, DELETE when using ?select= with empty bodies or ret=min

* fix select with empty bodies and ret=min

* improved tests for default cases, fixed computed overloaded columns on empty-body?select= PATCH
2020-04-13 12:32:35 -05:00
steve-chavez 052843ac9b Update BACKERS.md 2020-04-13 12:13:35 -05:00
Wolfgang WaltherandGitHub c524531784 Fix overloaded computed columns on RPC (#1473)
* fix some typos and spelling

* fix pg_source CTE name should be prefixed with pgrst_

* added tests for overloaded computed columns on patch calls
2020-04-06 14:04:55 -05:00
Wolfgang WaltherandGitHub 82fa1d8812 add postgres-12 tests to circleci (#1472)
* circleci: new postgres docker containers need host auth trust
2020-04-06 11:00:26 -05:00
steve-chavez 2b61a63686 Bump to version v7.0.0 2020-04-03 11:22:11 -05:00
steve-chavez 18e45659ea Fix compiling on windows(unix socket issue)
Group unix socket functionality into its own module
2020-04-03 11:22:11 -05:00
steve-chavez 426637a47c Fix circleci golang docker image
Upgrade golang otherwise ghr throws:
unrecognized import path "math/bits"
2020-04-03 11:22:11 -05:00
Steve ChavezandGitHub ababf7d4fa Change docker image to support scram-sha-256 auth (#1467)
Debian stretch has a libpq < 10: https://packages.debian.org/stretch/libpq5.
But buster has libpq >= 10 https://packages.debian.org/buster/libpq5.

Fixes https://github.com/PostgREST/postgrest/issues/1443.
2020-04-01 10:14:00 -05:00
691bb5640d Allow multiple schemas to be exposed in one instance (#1450)
The schema to use can be selected through the headers `Accept-Profile` for GET/HEAD and `Content-Profile` for POST/PATCH/PUT/DELETE.

This is based on the https://www.w3.org/TR/dx-prof-conneg/ttps://www.w3.org/TR/dx-prof-conneg/ spec.

Also increase all memory tests by 1M(otherwise CI fails).

Co-authored-by: Mahmoud Kassem <MKassem@gk-software.com>
Co-authored-by: Mahmoud Kassem <mahmoud_k@mail.com>
2020-03-30 14:04:20 -05:00
Wolfgang WaltherandGitHub a80eb2ff0e Add docker-compose file to run tests (#1463) 2020-03-24 09:50:06 -05:00
Duncan OgilvieandGitHub fe59f9bedf Improve command line help (#1465)
Make jwt-secret explicit about 32 characters minimum.
2020-03-21 18:44:08 -05:00
Steve ChavezandGitHub 0f8838623b Fix empty headers being added on POST/PATCH/DELETE (#1458) 2020-03-13 11:02:38 -05:00
RobertandGitHub 5b5945e427 Update configurator-pg to 0.2.0 (better error messages) (#1457)
* Don't indent first line of configuration parser error messages

configurator-pg returns multi-line errors now. Alternatively,
we could indent all the lines.
2020-03-12 10:04:44 -05:00
Robert VollmertandSteve Chavez dea57bd1be Raise lower bound for configurator-pg.
Building on GHC 8.8.3 requires the MonadFail instance that 0.1.0.6
provides.
2020-03-09 13:06:27 -05:00
Robert VollmertandSteve Chavez 3e81a38438 Relax upper bounds for GHC 8.8.3 / stackage lts-15.3.
Specifically, allow base 4.13 which ships with GHC 8.8, as well
as several dependency updates including hasql-transaction 1.0,
swagger 2.5 and warp 3.3.
2020-03-09 13:06:27 -05:00
Robert VollmertandSteve Chavez dfdf3d30b3 Fix test typo. 2020-03-07 13:04:17 -05:00
Robert VollmertandSteve Chavez 60b64d3e81 Upgrade hspec-wai dependency to 0.10. 2020-03-07 13:04:17 -05:00
Robert VollmertandSteve Chavez 962fba4d16 Update stack.yaml.lock for HSYaml extra-deps. 2020-03-07 13:04:17 -05:00
steve-chavez de218e900b Fix for sylish-haskell 2020-02-21 13:30:30 -05:00
steve-chavez b75e7cef90 Change server-proxy-uri 2 openapi-server-proxy-uri
The server-proxy-uri config option mislead into thinking it was a
web server global setting when it's only related to OpenAPI.
2020-02-02 15:23:32 -05:00
steve-chavez 9b1224827a Allow overriding headers Location,Content-Type,etc 2020-01-21 12:28:05 -05:00
steve-chavez c7f78fa7fc Add response.headers on GET/POST/PATCH/PUT/DELETE 2020-01-21 12:28:05 -05:00
steve-chavez 7dade7f466 Add request method and path GUCs 2020-01-21 12:28:05 -05:00
steve-chavez b20e1150a5 Add GUC for accessing the Authorization header 2020-01-21 12:28:05 -05:00
steve-chavez aa0d6a6831 Update BACKERS.md 2020-01-21 12:28:05 -05:00
Steve ChavezandGitHub 663faa1f82 Allow specifying the constraint name to disambiguate an embedding (#1430)
Makes previous duck typing regex unnecessary since the FK can be renamed
to a singular name or to any other format.

* Remove embedding with duck typed column names
* Allow embedding by foreign key name
* Add junction disambiguation tests
2020-01-06 09:42:33 -05:00
YUKISHITA YohsukeandSteve Chavez 99b13fa25f Upsert with "Prefer: resolution=merge-duplicates" on specified columns (#1327) (#1432)
Allow query param 'on_conflict=key1,key2,...' to upsert with explicit columns with unique
constraint.
2019-12-29 11:52:58 -05:00
Dmitry WaginandSteve Chavez e12c1319b6 Change default server-unix-socket-mode 755 -> 660 (#1423) 2019-12-16 08:37:00 -05:00
Dmitry WaginandSteve Chavez 7f365bf60b Enforce singular behavior despite Prefer: return (#1417) 2019-12-08 01:49:20 -05:00
Dan AmorosoandSteve Chavez 2e6c78d723 add config option server-unix-socket-mode (#1415)
* added config option server-unix-socket-mode to enable custom socket permissions

* added server-unix-socket-mode input validation
2019-11-23 10:52:33 -05:00
steve-chavez f9c64d9f65 refactor: improve disambiguation error message
* reverse backwards relationships
* remove redundancy from getJoinSelects
* properly name Cardinality constructors
2019-11-17 13:28:21 -05:00
steve-chavez 4ef6926791 Update authorship 2019-11-17 13:28:21 -05:00
Brian WignallandSteve Chávez 9645f1011c Fix double-and typo 2019-11-12 10:00:23 -05:00
steve-chavez 9847e60dca Err embedding when multiple relationships found
When having one-to-many relationships like:

person        -< message[sender]
person        -< message[recipient]
person_detail -< message[sender]
person_detail -< message[recipient]

Where person_detail is a view of person.

This request:

GET "/message?select=*,sender(*)"

Is ambiguous. Both person or person_detail could be embedded.

Until now we have returned the first detected relationship but
now we return a 300 Multiple Choices error with a
descriptive error message asking the user to disambiguate.

This is more helpful for the user and also aids in cases of more
complex relationships.
2019-11-05 12:55:48 -05:00
steve-chavez cb3d9ab625 refactor: rename RelationType to Cardinality
Remove Root type constructor
2019-11-05 12:55:48 -05:00
steve-chavez db41fb454e refactor: Rename synonyms to source columns 2019-11-05 12:55:48 -05:00
steve-chavez 3b133d5554 Fix #1397, correct compression to XZ for osx bin 2019-10-24 13:12:39 -05:00
Steve ChávezandGitHub 80f763448f Fix unique foreign key in view (#1395) 2019-10-16 12:45:38 -05:00
steve-chavez a3701f5de8 Update BACKERS.md 2019-10-14 14:31:29 -05:00
steve-chavez ed2bfc09a6 Allow PATCH/DELETE w/o Prefer when no SELECT privs
PATCH/DELETE can now be done without adding Prefer return=minimal when
the user doesn't have SELECT privileges.

* Also fix PATCH wrong HTTP status code
2019-10-08 12:41:39 -05:00
steve-chavez 337f821e00 refactor: remove parent embed workaround(in #647)
This workaround is no more necessary since the addition of #978.

* Also add a test for proving parent embeds offset is consistent
  with other types of embeds.
2019-10-08 12:41:39 -05:00
steve-chavez f2b126f147 refactor: Move hasql helpers to Common module 2019-10-08 12:41:39 -05:00
steve-chavez 1173bc277b refactor: rm configSchema ref in postgrest func
* make TargetDefaultSpec take a schema attribute

* remove schema param from addJoinConditions
2019-10-08 12:41:39 -05:00
steve-chavez 50f2cc16ab refactor: rm schema arg from QueryBuilder funcs
* Change TableName to QualifiedIdentifier in ReadQuery
  and MutateQuery.

* Move removeSourceCTESchema to DbRequestBuilder.
2019-10-08 12:41:39 -05:00
steve-chavez eebe319bfd Recommend doing make check pre-commit 2019-09-30 08:46:31 -05:00
steve-chavez 75a42b77ea Fix M2M resource embedding on RPC and mutations 2019-09-28 13:45:18 -05:00
steve-chavez d71d3450af Fix PATCH embed when not having the id in ?select 2019-09-28 13:45:18 -05:00
steve-chavez f080159268 refactor: add the returningCols function
* separate fieldNames from getting fkCols

* Put binaryField inside readSqlParts

* Move scalar proc logic to binaryField

* Move logic for the "SELECT *" default to DbRequestBuilder
2019-09-28 13:45:18 -05:00
steve-chavez 0183d32c7f refactor: Remove DbRequest type
This type only adds an extra constructor.
2019-09-28 13:45:18 -05:00
steve-chavez 94f5894d7f Fix self join resource embedding on PATCH 2019-09-28 13:45:18 -05:00
steve-chavez 81e5a62f25 refactor: remove fromJust in DbRequestBuilder
* Also comment and reorganize DbRequestBuilder
2019-09-28 13:45:18 -05:00
Steve ChávezandGitHub 186381bab2 Add support for Prefer: count=planned/estimated on GET /table (#1386) 2019-09-18 10:13:48 -05:00
steve-chavez e044488f73 Make costly bulk call query optional
Bulk Call should be used by specifying
the `Prefer: params=multiple-objects` header.
2019-09-11 12:01:41 -05:00
steve-chavez b077974ebc Add tests for call proc queries EXPLAIN costs
* circleci: add run query costs tests
2019-09-11 12:01:41 -05:00
steve-chavez 200540dfc3 refactor: put callProc core query to QueryBuilder
* Move set local queries to QueryBuilder

* Move unquoted to Middleware
2019-09-11 12:01:41 -05:00
steve-chavez 3c00f46e36 refactor: Move statements logic to own module
* Move gucHeader decoding to Statements
2019-09-11 12:01:41 -05:00
steve-chavez 620721dea7 Add support for HEAD request 2019-09-03 13:38:53 -05:00
steve-chavez 68cbe34c11 refactor: only run db reset on certain specs
This speeds up running the whole test suite
2019-09-03 13:38:53 -05:00
steve-chavez e21b010c6e refactor: reorganize modules
* Move range logic from App to RangeQuery

* Move decoder logic to Statement modules

* Move location headers logic to functions

* Add a comment for App.hs module
2019-09-03 13:38:53 -05:00
Robert VollmertandSteve Chávez 0846d4d7b2 stack.yaml: Document use of obscure ghc options. 2019-08-27 14:49:07 -05:00
Robert VollmertandSteve Chávez 2183a2a1ae stack.yaml: Remove no-warn-redundant-constraints.
The warning appears to not be part of -Wall anymore in recent
GHC versions.

Also move the warning-related options next to each other.
2019-08-27 14:49:07 -05:00
Robert VollmertandSteve Chávez e8475b18d3 Update stack.yaml.lock for lts-14.3 2019-08-27 09:35:27 -05:00
Robert VollmertandSteve Chávez cdc1177762 stylish-haskell: check Haskell source files only
The style check was failing due to a changed stack.yaml.lock file.

(That file still should be updated, but this is not where it should
cause a failure.)
2019-08-27 09:35:27 -05:00
Robert VollmertandSteve Chávez 97035e0b8b circleci: make sure hlint is in the $PATH 2019-08-27 09:35:27 -05:00
Robert VollmertandSteve Chávez aaf62c1c96 Build with -fno-spec-constr.
Compare https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
2019-08-27 09:35:27 -05:00
Robert VollmertandSteve Chávez 4d0661fd9b Upgrade swagger to 2.4.
Two changes:
- tags are now an insert-ordered hashset
- type_ .~ -> type_ ?~ with some magic type inference

This also updates stack.yaml to the newest GHC 8.6 LTS release.
2019-08-27 09:35:27 -05:00
101 changed files with 5551 additions and 2403 deletions
+206 -120
View File
@@ -1,66 +1,69 @@
version: 2 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: jobs:
build-cache:
docker:
- image: circleci/buildpack-deps:trusty
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/v2.1.3/stack-2.1.3-linux-x86_64.tar.gz | tar zx -C /tmp
sudo mv /tmp/stack-2.1.3-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
- run:
name: build src and tests dependencies
command: |
stack build --fast -j1 --only-dependencies
stack build --fast --test --no-run-tests --only-dependencies
- save_cache:
paths:
- "~/.stack"
- ".stack-work"
key: v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
- run:
name: build src and tests
command: |
stack build --fast -j1
stack build --fast --test --no-run-tests
style:
docker:
- image: nixos/nix:2.3
steps:
- checkout
- run:
name: Install linting and styling scripts
command: nix-env -f default.nix -iA lint style
- run:
name: Run linter
command: |
echo 'Note: For checking this locally, use `make lint`. Or, if using `nix`, run `nix-shell --run postgrest-lint`'
postgrest-lint
- run:
name: Run style check
command: |
echo 'Note: For checking this locally, use `make style`. Or, if using `nix`, run `nix-shell --run postgrest-style`'
postgrest-style-check
build-test-9.4: build-test-9.4:
docker: docker:
- image: circleci/buildpack-deps:trusty - image: circleci/buildpack-deps:trusty
environment: environment:
- PGHOST=localhost - PGHOST=localhost
- image: circleci/postgres:9.4.14 - image: circleci/postgres:9.4.26
environment: environment:
- POSTGRES_USER=circleci - POSTGRES_USER=circleci
- POSTGRES_DB=circleci - POSTGRES_DB=circleci
- POSTGRES_HOST_AUTH_METHOD=trust
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@@ -81,18 +84,11 @@ jobs:
sudo apt-get install -y --only-upgrade binutils sudo apt-get install -y --only-upgrade binutils
sudo apt-get install -y postgresql-client sudo apt-get install -y postgresql-client
stack setup stack setup
rm -rf $(stack path --dist-dir) $(stack path --local-install-root)
stack install hlint stylish-haskell
- run: - run:
name: build src and tests dependencies name: build src and tests dependencies
command: | command: |
stack build --fast -j1 --only-dependencies stack build --fast -j1 --only-dependencies
stack build --fast --test --no-run-tests --only-dependencies stack build --fast --test --no-run-tests --only-dependencies
- save_cache:
paths:
- "~/.stack"
- ".stack-work"
key: v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
- run: - run:
name: build src and tests name: build src and tests
command: | command: |
@@ -103,22 +99,17 @@ jobs:
command: | command: |
POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test
test/io-tests.sh 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: run styler
command: git ls-files | grep '\.l\?hs$' | xargs stack exec -- stylish-haskell -i && git diff-index --exit-code HEAD --
build-test-9.6: build-test-9.6:
docker: docker:
- image: circleci/buildpack-deps:trusty - image: circleci/buildpack-deps:trusty
environment: environment:
- PGHOST=localhost - PGHOST=localhost
- image: circleci/postgres:9.6.2 - image: circleci/postgres:9.6.17
environment: environment:
- POSTGRES_USER=circleci - POSTGRES_USER=circleci
- POSTGRES_DB=circleci - POSTGRES_DB=circleci
- POSTGRES_HOST_AUTH_METHOD=trust
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@@ -140,17 +131,18 @@ jobs:
stack build --fast --test --no-run-tests stack build --fast --test --no-run-tests
- run: - run:
name: run tests name: run tests
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test postgrest:spec
build-test-10: build-test-10:
docker: docker:
- image: circleci/buildpack-deps:trusty - image: circleci/buildpack-deps:trusty
environment: environment:
- PGHOST=localhost - PGHOST=localhost
- image: circleci/postgres:10.5 - image: circleci/postgres:10.12
environment: environment:
- POSTGRES_USER=circleci - POSTGRES_USER=circleci
- POSTGRES_DB=circleci - POSTGRES_DB=circleci
- POSTGRES_HOST_AUTH_METHOD=trust
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@@ -172,17 +164,18 @@ jobs:
stack build --fast --test --no-run-tests stack build --fast --test --no-run-tests
- run: - run:
name: run tests name: run tests
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test postgrest:spec
build-test-11: build-test-11:
docker: docker:
- image: circleci/buildpack-deps:trusty - image: circleci/buildpack-deps:trusty
environment: environment:
- PGHOST=localhost - PGHOST=localhost
- image: circleci/postgres:11.4 - image: circleci/postgres:11.7
environment: environment:
- POSTGRES_USER=circleci - POSTGRES_USER=circleci
- POSTGRES_DB=circleci - POSTGRES_DB=circleci
- POSTGRES_HOST_AUTH_METHOD=trust
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@@ -204,7 +197,40 @@ jobs:
stack build --fast --test --no-run-tests stack build --fast --test --no-run-tests
- run: - run:
name: run tests name: run tests
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test postgrest:spec
build-test-12:
docker:
- image: circleci/buildpack-deps:trusty
environment:
- PGHOST=localhost
- image: circleci/postgres:12.2
environment:
- POSTGRES_USER=circleci
- POSTGRES_DB=circleci
- POSTGRES_HOST_AUTH_METHOD=trust
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/v2.1.3/stack-2.1.3-linux-x86_64.tar.gz | tar zx -C /tmp
sudo mv /tmp/stack-2.1.3-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 postgrest:spec
build-prof-test: build-prof-test:
docker: docker:
@@ -212,10 +238,11 @@ jobs:
environment: environment:
- PGHOST=localhost - PGHOST=localhost
- TERM=xterm - TERM=xterm
- image: circleci/postgres:9.6.2 - image: circleci/postgres:9.6.17
environment: environment:
- POSTGRES_USER=circleci - POSTGRES_USER=circleci
- POSTGRES_DB=circleci - POSTGRES_DB=circleci
- POSTGRES_HOST_AUTH_METHOD=trust
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@@ -247,26 +274,11 @@ jobs:
name: run memory usage tests name: run memory usage tests
command: | command: |
test/create_test_db "postgres://circleci@localhost" postgrest_test 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/jsonschema.sql
psql "postgres:///postgrest_test" -f test/fixtures/privileges.sql
test/memory-tests.sh test/memory-tests.sh
centos7:
<<: *build-distro-bin
ubuntu:
<<: *build-distro-bin
ubuntui386:
<<: *build-distro-bin
release: release:
docker: docker:
- image: circleci/golang:1.8 - image: circleci/golang:1.9
steps: steps:
- attach_workspace: - attach_workspace:
at: /tmp/workspace at: /tmp/workspace
@@ -290,60 +302,132 @@ jobs:
docker tag postgrest postgrest/postgrest:latest docker tag postgrest postgrest/postgrest:latest
docker push postgrest/postgrest:latest docker push postgrest/postgrest:latest
build-nix:
machine: true
steps:
- checkout
- run:
name: Install Nix
command: |
curl -L https://nixos.org/nix/install | sh
echo "source $HOME/.nix-profile/etc/profile.d/nix.sh" >> $BASH_ENV
- run:
name: Install and use the Cachix binary cache
command: |
nix-env -iA cachix -f https://cachix.org/api/v1/install
cachix use postgrest
- run:
name: Build the dynamic PostgREST executable
command: |
# We build the dynamic postgrest executable first in order to fail fast if anything
# should be wrong with the source code.
nix-build -A postgrest
- restore_cache:
keys:
- v1-cabal-nix-dependencies-{{ checksum "postgrest.cabal" }}
- run:
name: Run the spec tests against all supported PostgreSQL versions
command: |
# Our utility scripts use cabal to run the tests, which will pick up all the libraries that
# are built with Nix. So this should only ever build the PostgREST itself and be reasonably
# quick. We accelerate that part further by caching `~/.cabal` and `dist-newstyle`.
nix-shell --run postgrest-test-spec-all
- save_cache:
paths:
- "~/.cabal"
- "dist-newstyle"
key: v1-cabal-nix-dependencies-{{ checksum "postgrest.cabal" }}
- run:
name: Build all derivations from default.nix and push results to Cachix
command: |
# Only push to the cache when CircleCI makes the CACHIX_SIGNING_KEY available (e.g. not for
# pull requests).
if [ -n "${CACHIX_SIGNING_KEY:-""}" ]; then
echo "Building and caching all derivations..."
nix-build | cachix push postgrest
else
echo "Building all derivations (caching skipped for outside pull requests)..."
nix-build
fi
build-static-bin:
machine: true
steps:
- checkout
- run:
name: Install Nix
command: |
curl -L https://nixos.org/nix/install | sh
echo "source $HOME/.nix-profile/etc/profile.d/nix.sh" >> $BASH_ENV
- run:
name: Install and use the Cachix binary cache
command: |
nix-env -iA cachix -f https://cachix.org/api/v1/install
cachix use postgrest
- run:
name: Build the PostgREST statically linked binary
command: |
nix-build -A postgrestStatic
- run:
name: compress binary
command: |
mkdir -p /tmp/workspace/bin
cd /tmp/workspace/bin
tar cvJf postgrest-$CIRCLE_TAG-linux-x64-static.tar.xz -C ~/project/result/bin postgrest
- persist_to_workspace:
root: /tmp/workspace
paths:
- bin/*
workflows: workflows:
version: 2 version: 2
build-test-release: build-test-release:
jobs: jobs:
- build-cache:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
- style:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
- build-test-9.4: - build-test-9.4:
filters: filters:
tags: tags:
only: /v[0-9]+(\.[0-9]+)*/ only: /v[0-9]+(\.[0-9]+)*/
requires:
- build-cache
- build-test-9.6: - build-test-9.6:
filters: filters:
tags: tags:
only: /v[0-9]+(\.[0-9]+)*/ only: /v[0-9]+(\.[0-9]+)*/
requires:
- build-cache
- build-test-10: - build-test-10:
filters: filters:
tags: tags:
only: /v[0-9]+(\.[0-9]+)*/ only: /v[0-9]+(\.[0-9]+)*/
requires:
- build-cache
- build-test-11: - build-test-11:
filters: filters:
tags: tags:
only: /v[0-9]+(\.[0-9]+)*/ only: /v[0-9]+(\.[0-9]+)*/
requires:
- build-cache
- build-test-12:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
requires:
- build-cache
- build-prof-test: - build-prof-test:
filters: filters:
tags: tags:
only: /v[0-9]+(\.[0-9]+)*/ only: /v[0-9]+(\.[0-9]+)*/
- centos7: - build-static-bin:
requires: requires:
- build-test-9.4 - build-nix
- build-test-9.6
- build-test-10
- build-test-11
- 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-test-11
- 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-test-11
- build-prof-test - build-prof-test
filters: filters:
tags: tags:
@@ -352,9 +436,11 @@ workflows:
ignore: /.*/ ignore: /.*/
- release: - release:
requires: requires:
- centos7 - build-static-bin
- ubuntu filters:
- ubuntui386 tags:
only: /v[0-9]+(\.[0-9]+)*/
- build-nix:
filters: filters:
tags: tags:
only: /v[0-9]+(\.[0-9]+)*/ only: /v[0-9]+(\.[0-9]+)*/
+55
View File
@@ -0,0 +1,55 @@
freebsd_instance:
image: freebsd-12-1-release-amd64
build_task:
env:
ASSUME_ALWAYS_YES: YES
install_script:
## TODO: Some of these workaround steps might be unneeded now
## gmake required, otherwise bsd make is used and some cryptic "No operator" errors are shown
- pkg install -y gmake
## gcc required, otherwise warning: macro expansion producing 'defined' has undefined behavior [-Wexpansion-to-defined]
- pkg install -y gcc
- pkg install -y libiconv
- pkg install -y gmp
- pkg install -y postgresql95-client
- pkg install -y pkgconf
- pkg install -y ghc
- pkg install -y hs-cabal-install
cabal_cache:
folder: ~/.cabal
fingerprint_script: cat postgrest.cabal
build_script:
- cabal v2-update
- cabal v2-build
binaries_artifacts:
path: "dist-newstyle/build/x86_64-freebsd/ghc-8.6.5/postgrest-7.0.1/x/postgrest/build/postgrest/*"
#binary_script:
#- mkdir bin
## cabal v2-install --installdir=.
## the installdir option is not working on this cabal version(form hs-cabal-install), so the binary must be extrated from the path
## this is too brittle so for now we don't use it
#- ldd /tmp/cirrus-ci-build/dist-newstyle/build/x86_64-freebsd/ghc-8.6.5/postgrest-7.0.1/x/postgrest/build/postgrest/postgrest
#- mv /tmp/cirrus-ci-build/dist-newstyle/build/x86_64-freebsd/ghc-8.6.5/postgrest-7.0.1/x/postgrest/build/postgrest/postgrest bin/
## https://cirrus-ci.org/guide/writing-tasks/#artifacts-instruction
#binaries_artifacts:
#path: bin/
cleanup_script:
- rm -rf ~/.cabal/logs
- rm ~/.cabal/config
#publish_task:
#skip: $CIRRUS_TAG == ""
#env:
#TOKEN: ENCRYPTED[c99babbf04e8a33962ffbbc75bf20d3abe408f9bede5ed5d8956cd24da6fdb34266c920984cc43f8106ef3423fba0e98]
#depends_on:
#- build
#script:
## https://cirrus-ci.org/guide/writing-tasks/#artifacts-instruction
#- wget https://api.cirrus-ci.com/v1/artifact/github/$CIRRUS_REPO_FULL_NAME/build/binaries/bin/postgrest
#- tar cjf postgrest-$CIRRUS_TAG-freebsd.tar.xz postgrest
### The CIRRUS_RELEASE env var is not the same as the tag. It's an id for the release.
#- curl -X POST --data-binary @$NAME \
#-H "Authorization:token ${TOKEN}" \
#-H "Content-Type:application/octet-stream" \
#"https://uploads.github.com/repos/$CIRRUS_REPO_FULL_NAME/releases/${CIRRUS_RELEASE}/assets?name=${NAME}"
+5 -6
View File
@@ -42,14 +42,13 @@ your contributions.
* All contributions must pass the tests before being merged. When * All contributions must pass the tests before being merged. When
you create a pull request your code will automatically be tested. you create a pull request your code will automatically be tested.
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/) * All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/) and [stylish-haskell](https://github.com/jaspervdj/stylish-haskell)
with no warnings. This helps enforce a uniform style for all with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every
committers. Continuous integration will check this as well on every pull request. There's a useful Makefile that helps with checking this locally. You can run `make commit-check` to do this manually but
pull request. we recommend adding it to `.git/hooks/pre-commit` to automatically check this before doing a commit.
* For help building the Haskell code on your computer check out the [building from * For help building the Haskell code on your computer check out the [building from
source](https://postgrest.com/en/stable/install.html#build-from-source) source](https://postgrest.com/en/stable/install.html#build-from-source) docs section.
wiki page.
### Running Tests ### Running Tests
+3
View File
@@ -12,3 +12,6 @@ site
*~ *~
*#* *#*
.#* .#*
*.swp
result*
dist-newstyle
+1 -1
View File
@@ -65,6 +65,6 @@ script:
END='## \[' END='## \['
BODY=$(sed -n "1,/$START/d;/$END/q;p" CHANGELOG.md) BODY=$(sed -n "1,/$START/d;/$END/q;p" CHANGELOG.md)
strip postgrest strip postgrest
tar cjf postgrest-$TRAVIS_TAG-osx.tar.xz 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 ghr -t $GITHUB_TOKEN -u $OWNER -r $REPO -b "$BODY"--replace $TRAVIS_TAG postgrest-$TRAVIS_TAG-osx.tar.xz
fi fi
+4
View File
@@ -30,6 +30,7 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
- [Daniel Babiak](https://github.com/d-babiak) - [Daniel Babiak](https://github.com/d-babiak)
- Evans Fernandes - Evans Fernandes
- [Jan Sommer](https://github.com/nerfpops)
## Backers ## Backers
@@ -41,6 +42,9 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
- Nicholas DiBiase - Nicholas DiBiase
- Christopher Reid - Christopher Reid
- Nathan Bouscal - Nathan Bouscal
- Daniel Rafaj
- David Fenko
- Remo Rechkemmer
## Former Backers ## Former Backers
+42
View File
@@ -9,6 +9,48 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed ### Fixed
## [7.0.1] - 2020-05-18
### Fixed
- #1473, Fix overloaded computed columns on RPC - @wolfgangwalther
- #1471, Fix POST, PATCH, DELETE with ?select= and return=minimal and PATCH with empty body - @wolfgangwalther
- #1500, Fix missing `openapi-server-proxy-uri` config option - @steve-chavez
- #1508, Fix `Content-Profile` not working for POST RPC - @steve-chavez
- #1452, Fix PUT restriction for all columns - @steve-chavez
## [7.0.0] - 2020-04-03
### Added
- #1417, `Accept: application/vnd.pgrst.object+json` behavior is now enforced for POST/PATCH/DELETE regardless of `Prefer: return=representation/minimal` - @dwagin
- #1415, Add support for user defined socket permission via `server-unix-socket-mode` config option - @Dansvidania
- #1383, Add support for HEAD request - @steve-chavez
- #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez, @LorenzHenk
- #1327, Add support for optional query parameter `on_conflict` to upsert with specified keys for POST - @ykst
- #1430, Allow specifying the foreign key constraint name(`/source?select=fk_constraint(*)`) to disambiguate an embedding - @steve-chavez
- #1168, Allow access to the `Authorization` header through the `request.header.authorization` GUC - @steve-chavez
- #1435, Add `request.method` and `request.path` GUCs - @steve-chavez
- #1088, Allow adding headers to GET/POST/PATCH/PUT/DELETE responses through the `response.headers` GUC - @steve-chavez
- #1427, Allow overriding provided headers(Location, Content-Type, etc) through the `response.headers` GUC - @steve-chavez
- #1450, Allow multiple schemas to be exposed in one instance. The schema to use can be selected through the headers `Accept-Profile` for GET/HEAD and `Content-Profile` for POST/PATCH/PUT/DELETE - @steve-chavez, @mahmoudkassem
### Fixed
- #1301, Fix self join resource embedding on PATCH - @herulume, @steve-chavez
- #1389, Fix many to many resource embedding on RPC/PATCH - @steve-chavez
- #1355, Allow PATCH/DELETE without `return=minimal` on tables with no select privileges - @steve-chavez
- #1361, Fix embedding a VIEW when its source foreign key is UNIQUE - @bwbroersma
### Changed
- #1385, bulk RPC call now should be done by specifying a `Prefer: params=multiple-objects` header - @steve-chavez
- #1401, resource embedding now outputs an error when multiple relationships between two tables are found - @steve-chavez
- #1423, default Unix Socket file mode from 755 to 660 - @dwagin
- #1430, Remove embedding with duck typed column names `GET /projects?select=client(*)`- @steve-chavez
+ You can rename the foreign key to `client` to make this request work in the new version: `alter table projects rename constraint projects_client_id_fkey to client`
- #1413, Change `server-proxy-uri` config option to `openapi-server-proxy-uri` - @steve-chavez
## [6.0.2] - 2019-08-22 ## [6.0.2] - 2019-08-22
### Fixed ### Fixed
+1
View File
@@ -1,4 +1,5 @@
Copyright (c) 2014 Joe Nelson Copyright (c) 2014 Joe Nelson
Copyright (c) 2019 Steve Chavez
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the
+6 -5
View File
@@ -1,16 +1,19 @@
.PHONY: check clean lint style test test-watch coverage circleci circleci-prof-test check-dburi prompt-clean prompt-long-process .PHONY: commit-check check clean lint style test test-watch coverage circleci circleci-prof-test check-dburi prompt-clean prompt-long-process
commit-check: lint style
check: lint style test check: lint style test
clean: prompt-clean clean: prompt-clean
stack clean --full stack clean --full
# For running these you'll need to install hlint and stylish-haskell first. Run:
# stack install hlint stylish-haskell
lint: lint:
git ls-files | grep '\.l\?hs$$' | xargs stack exec -- hlint -X QuasiQuotes -X NoPatternSynonyms "$$@" git ls-files | grep '\.l\?hs$$' | xargs stack exec -- hlint -X QuasiQuotes -X NoPatternSynonyms "$$@"
style: style:
git ls-files | grep '\.l\?hs$$' | xargs stack exec -- stylish-haskell -i git ls-files | grep '\.l\?hs$$' | xargs stack exec -- stylish-haskell -i && git diff-index --exit-code HEAD -- '*.hs' '*.lhs'
test: check-dburi test: check-dburi
stack test stack test
@@ -22,14 +25,12 @@ coverage: check-dburi clean
stack build --coverage stack build --coverage
stack test --coverage stack test --coverage
circleci: prompt-long-process circleci: prompt-long-process
circleci local execute --job build-test-9.4 circleci local execute --job build-test-9.4
circleci-prof-test: prompt-long-process circleci-prof-test: prompt-long-process
circleci local execute --job build-prof-test circleci local execute --job build-prof-test
check-dburi: check-dburi:
test -n "$(POSTGREST_TEST_CONNECTION)" # Requires POSTGREST_TEST_CONNECTION environmental variable test -n "$(POSTGREST_TEST_CONNECTION)" # Requires POSTGREST_TEST_CONNECTION environmental variable
+1 -1
View File
@@ -10,7 +10,7 @@
}, },
"POSTGREST_VER": { "POSTGREST_VER": {
"description": "Version of PostgREST to deploy", "description": "Version of PostgREST to deploy",
"value": "6.0.2" "value": "7.0.1"
}, },
"DB_URI": { "DB_URI": {
"description": "Database connection string, e.g. postgres://user:pass@xxxxxxx.rds.amazonaws.com/mydb", "description": "Database connection string, e.g. postgres://user:pass@xxxxxxx.rds.amazonaws.com/mydb",
+1
View File
@@ -1,5 +1,6 @@
## AppVeyor is only used for building a Windows binary, no tests are run here. ## AppVeyor is only used for building a Windows binary, no tests are run here.
platform: x64 platform: x64
image: Visual Studio 2015
cache: cache:
- "c:\\sr" - "c:\\sr"
+109
View File
@@ -0,0 +1,109 @@
let
name =
"postgrest";
compiler =
"ghc883";
# PostgREST source files, filtered based on the rules in the .gitignore files
# and file extensions. We want to include as litte as possible, as the files
# added here will increase the space used in the Nix store and trigger the
# build of new Nix derivations when changed.
src =
pkgs.lib.sourceFilesBySuffices
(pkgs.gitignoreSource ./.)
[ ".cabal" ".hs" ".lhs" "LICENSE" ];
# Commit of the Nixpkgs repository that we want to use.
nixpkgsVersion =
import nix/nixpkgs-version.nix;
# Nix files that describe the Nixpkgs repository. We evaluate the expression
# using `import` below.
nixpkgs =
builtins.fetchTarball {
url = "https://github.com/nixos/nixpkgs/archive/${nixpkgsVersion.rev}.tar.gz";
sha256 = nixpkgsVersion.tarballHash;
};
overlays =
[
(import nix/overlays/postgresql-legacy.nix)
(import nix/overlays/gitignore.nix)
(import nix/overlays/haskell-packages)
];
# Evaluated expression of the Nixpkgs repository.
pkgs =
import nixpkgs { inherit overlays; };
postgresqlVersions =
[
pkgs.postgresql_12
pkgs.postgresql_11
pkgs.postgresql_10
pkgs.postgresql_9_6
pkgs.postgresql_9_5
pkgs.postgresql_9_4
];
patches =
pkgs.callPackage nix/patches {};
# Base dynamic derivation for the PostgREST package.
drv =
pkgs.haskellPackages.callCabal2nix name src {};
# Static derivation for the PostgREST executable.
drvStatic =
import nix/static { inherit nixpkgs name src compiler patches; };
lib =
pkgs.haskell.lib;
in
rec {
inherit nixpkgs pkgs;
# Derivation for the PostgREST Haskell package, including the executable,
# libraries and documentation. We disable running the test suite on Nix
# builds, as they require a database to be set up.
postgrestWithLib =
lib.dontCheck drv;
# Derivation for just the PostgREST binary, where we strip all dynamic
# libraries and documentation, leaving only the executable. Note that the
# executable is static with regards to Haskell libraries, but not system
# libraries like glibc and libpq.
postgrest =
lib.justStaticExecutables postgrestWithLib;
# Static executable.
postgrestStatic =
lib.justStaticExecutables (lib.dontCheck drvStatic);
# Docker image and loading script.
docker =
pkgs.callPackage nix/docker { postgrest = postgrestStatic; };
# Environment in which PostgREST can be built with cabal, useful e.g. for
# defining a shell for nix-shell.
env =
drv.env;
# Utility for updating the pinned version of Nixpkgs.
nixpkgsUpgrade =
pkgs.callPackage nix/nixpkgs-upgrade.nix {};
tests =
pkgs.callPackage nix/tests.nix
{
inherit postgresqlVersions;
postgrestBuildEnv = env;
};
style =
pkgs.callPackage nix/style.nix {};
lint =
pkgs.callPackage nix/lint.nix {};
}
+5 -2
View File
@@ -1,4 +1,7 @@
FROM debian:stretch-slim # To build use:
# docker build --build-arg POSTGREST_VERSION=<v5.2.0 or another version> -t postgrest ./docker/
FROM debian:buster-slim
ARG POSTGREST_VERSION ARG POSTGREST_VERSION
@@ -31,7 +34,7 @@ ENV PGRST_DB_URI= \
PGRST_DB_EXTRA_SEARCH_PATH=public \ PGRST_DB_EXTRA_SEARCH_PATH=public \
PGRST_SERVER_HOST=*4 \ PGRST_SERVER_HOST=*4 \
PGRST_SERVER_PORT=3000 \ PGRST_SERVER_PORT=3000 \
PGRST_SERVER_PROXY_URI= \ PGRST_OPENAPI_SERVER_PROXY_URI= \
PGRST_JWT_SECRET= \ PGRST_JWT_SECRET= \
PGRST_SECRET_IS_BASE64=false \ PGRST_SECRET_IS_BASE64=false \
PGRST_JWT_AUD= \ PGRST_JWT_AUD= \
+1 -1
View File
@@ -7,7 +7,7 @@ db-extra-search-path = "$(PGRST_DB_EXTRA_SEARCH_PATH)"
server-host = "$(PGRST_SERVER_HOST)" server-host = "$(PGRST_SERVER_HOST)"
server-port = "$(PGRST_SERVER_PORT)" server-port = "$(PGRST_SERVER_PORT)"
server-proxy-uri = "$(PGRST_SERVER_PROXY_URI)" openapi-server-proxy-uri = "$(PGRST_OPENAPI_SERVER_PROXY_URI)"
jwt-secret = "$(PGRST_JWT_SECRET)" jwt-secret = "$(PGRST_JWT_SECRET)"
secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)" secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)"
jwt-aud = "$(PGRST_JWT_AUD)" jwt-aud = "$(PGRST_JWT_AUD)"
+41 -48
View File
@@ -12,25 +12,16 @@ import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
import Control.Retry (RetryStatus, capDelay, import Control.Retry (RetryStatus, capDelay,
exponentialBackoff, retrying, exponentialBackoff, retrying,
rsPreviousDelay) rsPreviousDelay)
import Data.Either.Combinators (whenLeft)
import Data.IORef (IORef, atomicWriteIORef, newIORef, import Data.IORef (IORef, atomicWriteIORef, newIORef,
readIORef) readIORef)
import Data.String (IsString (..)) import Data.String (IsString (..))
import Data.Text (pack, replace, strip, stripPrefix, import Data.Text (pack, replace, strip, stripPrefix)
unpack) import Data.Text.IO (hPutStrLn)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Text.IO (hPutStrLn, readFile)
import Data.Time.Clock (getCurrentTime) import Data.Time.Clock (getCurrentTime)
import Network.Socket (Family (AF_UNIX),
SockAddr (SockAddrUnix), Socket,
SocketType (Stream), bind, close,
defaultProtocol, listen,
maxListenQueue, socket)
import Network.Wai.Handler.Warp (defaultSettings, runSettings, import Network.Wai.Handler.Warp (defaultSettings, runSettings,
runSettingsSocket, setHost, setPort, setHost, setPort, setServerName)
setServerName)
import System.Directory (removeFile)
import System.IO (BufferMode (..), hSetBuffering) import System.IO (BufferMode (..), hSetBuffering)
import System.IO.Error (isDoesNotExistError)
import PostgREST.App (postgrest) import PostgREST.App (postgrest)
import PostgREST.Config (AppConfig (..), configPoolTimeout', import PostgREST.Config (AppConfig (..), configPoolTimeout',
@@ -42,13 +33,16 @@ import PostgREST.OpenAPI (isMalformedProxyUri)
import PostgREST.Types (ConnectionStatus (..), DbStructure, import PostgREST.Types (ConnectionStatus (..), DbStructure,
PgVersion (..), Schema, PgVersion (..), Schema,
minimumPgVersion) minimumPgVersion)
import Protolude hiding (hPutStrLn, replace) import Protolude hiding (hPutStrLn, head, replace, toS)
import Protolude.Conv (toS)
#ifndef mingw32_HOST_OS #ifndef mingw32_HOST_OS
import System.Posix.Signals import System.Posix.Signals
import UnixSocket
#endif #endif
{-| {-|
The purpose of this worker is to fill the refDbStructure created in 'main' The purpose of this worker is to fill the refDbStructure created in 'main'
with the 'DbStructure' returned from calling 'getDbStructure'. This method with the 'DbStructure' returned from calling 'getDbStructure'. This method
@@ -70,11 +64,11 @@ import System.Posix.Signals
connectionWorker connectionWorker
:: ThreadId -- ^ This thread is killed if pg version is unsupported :: ThreadId -- ^ This thread is killed if pg version is unsupported
-> P.Pool -- ^ The PostgreSQL connection pool -> P.Pool -- ^ The PostgreSQL connection pool
-> Schema -- ^ Schema PostgREST is serving up -> [Schema] -- ^ Schemas PostgREST is serving up
-> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure' -> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure'
-> IORef Bool -- ^ Used as a binary Semaphore -> IORef Bool -- ^ Used as a binary Semaphore
-> IO () -> IO ()
connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do connectionWorker mainTid pool schemas refDbStructure refIsWorkerOn = do
isWorkerOn <- readIORef refIsWorkerOn isWorkerOn <- readIORef refIsWorkerOn
unless isWorkerOn $ do unless isWorkerOn $ do
atomicWriteIORef refIsWorkerOn True atomicWriteIORef refIsWorkerOn True
@@ -90,7 +84,7 @@ connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do
NotConnected -> return () -- Unreachable NotConnected -> return () -- Unreachable
Connected actualPgVersion -> do -- Procede with initialization Connected actualPgVersion -> do -- Procede with initialization
result <- P.use pool $ do result <- P.use pool $ do
dbStructure <- HT.transaction HT.ReadCommitted HT.Read $ getDbStructure schema actualPgVersion dbStructure <- HT.transaction HT.ReadCommitted HT.Read $ getDbStructure schemas actualPgVersion
liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure
case result of case result of
Left e -> do Left e -> do
@@ -159,10 +153,12 @@ main = do
-- readOptions builds the 'AppConfig' from the config file specified on the -- readOptions builds the 'AppConfig' from the config file specified on the
-- command line -- command line
conf <- loadDbUriFile =<< loadSecretFile =<< readOptions conf <- loadDbUriFile =<< loadSecretFile =<< readOptions
let host = configHost conf let schemas = toList $ configSchemas conf
host = configHost conf
port = configPort conf port = configPort conf
proxy = configProxyUri conf proxy = configOpenAPIProxyUri conf
maybeSocketAddr = configSocket conf maybeSocketAddr = configSocket conf
socketFileMode = configSocketMode conf
pgSettings = toS (configDatabase conf) -- is the db-uri pgSettings = toS (configDatabase conf) -- is the db-uri
roleClaimKey = configRoleClaimKey conf roleClaimKey = configRoleClaimKey conf
appSettings = appSettings =
@@ -171,16 +167,18 @@ main = do
. setServerName (toS $ "postgrest/" <> prettyVersion) $ . setServerName (toS $ "postgrest/" <> prettyVersion) $
defaultSettings defaultSettings
whenLeft socketFileMode panic
-- Checks that the provided proxy uri is formated correctly -- Checks that the provided proxy uri is formated correctly
when (isMalformedProxyUri $ toS <$> proxy) $ when (isMalformedProxyUri $ toS <$> proxy) $
panic panic
"Malformed proxy uri, a correct example: https://example.com:8443/basePath" "Malformed proxy uri, a correct example: https://example.com:8443/basePath"
-- Checks that the provided jspath is valid -- Checks that the provided jspath is valid
when (isLeft roleClaimKey) $ whenLeft roleClaimKey $
panic $ show roleClaimKey panic $ show roleClaimKey
--
-- create connection pool with the provided settings, returns either -- create connection pool with the provided settings, returns either
-- a 'Connection' or a 'ConnectionError'. Does not throw. -- a 'Connection' or a 'ConnectionError'. Does not throw.
pool <- P.acquire (configPool conf, configPoolTimeout' conf, pgSettings) pool <- P.acquire (configPool conf, configPoolTimeout' conf, pgSettings)
@@ -199,7 +197,7 @@ main = do
connectionWorker connectionWorker
mainTid mainTid
pool pool
(configSchema conf) schemas
refDbStructure refDbStructure
refIsWorkerOn refIsWorkerOn
-- --
@@ -221,7 +219,7 @@ main = do
Catch $ connectionWorker Catch $ connectionWorker
mainTid mainTid
pool pool
(configSchema conf) schemas
refDbStructure refDbStructure
refIsWorkerOn refIsWorkerOn
) Nothing ) Nothing
@@ -240,22 +238,20 @@ main = do
(connectionWorker (connectionWorker
mainTid mainTid
pool pool
(configSchema conf) schemas
refDbStructure refDbStructure
refIsWorkerOn) refIsWorkerOn)
in case maybeSocketAddr of
Nothing -> do -- run the postgrest application with user defined socket. Only for UNIX systems.
-- run the postgrest application #ifndef mingw32_HOST_OS
putStrLn $ ("Listening on port " :: Text) <> show (configPort conf) whenJust maybeSocketAddr $
runSettings appSettings postgrestApplication runAppInSocket appSettings postgrestApplication socketFileMode
Just socketAddr -> do #endif
-- run postgrest application with user defined socket
sock <- createAndBindSocket (unpack socketAddr) -- run the postgrest application
listen sock maxListenQueue whenNothing maybeSocketAddr $ do
putStrLn $ ("Listening on unix socket " :: Text) <> show socketAddr putStrLn $ ("Listening on port " :: Text) <> show (configPort conf)
runSettingsSocket appSettings sock postgrestApplication runSettings appSettings postgrestApplication
-- clean socket up when done
close sock
{-| {-|
The purpose of this function is to load the JWT secret from a file if The purpose of this function is to load the JWT secret from a file if
@@ -324,14 +320,11 @@ loadDbUriFile conf = extractDbUri mDbUri
Just filename -> strip <$> readFile (toS filename) Just filename -> strip <$> readFile (toS filename)
setDbUri dbUri = conf {configDatabase = dbUri} setDbUri dbUri = conf {configDatabase = dbUri}
createAndBindSocket :: FilePath -> IO Socket -- Utilitarian functions.
createAndBindSocket filePath = do whenJust :: Applicative f => Maybe a -> (a -> f ()) -> f ()
deleteSocketFileIfExist filePath whenJust (Just x) f = f x
sock <- socket AF_UNIX Stream defaultProtocol whenJust Nothing _ = pass
bind sock $ SockAddrUnix filePath
return sock whenNothing :: Applicative f => Maybe a -> f () -> f ()
where whenNothing Nothing f = f
deleteSocketFileIfExist path = removeFile path `catch` handleDoesNotExist whenNothing _ _ = pass
handleDoesNotExist e
| isDoesNotExistError e = return ()
| otherwise = throwIO e
+40
View File
@@ -0,0 +1,40 @@
module UnixSocket (
runAppInSocket
)where
import Network.Socket (Family (AF_UNIX),
SockAddr (SockAddrUnix), Socket,
SocketType (Stream), bind, close,
defaultProtocol, listen,
maxListenQueue, socket)
import Network.Wai (Application)
import Network.Wai.Handler.Warp
import System.Directory (removeFile)
import System.IO.Error (isDoesNotExistError)
import System.Posix.Files (setFileMode)
import System.Posix.Types (FileMode)
import Protolude
createAndBindSocket :: FilePath -> Maybe FileMode -> IO Socket
createAndBindSocket socketFilePath maybeSocketFileMode = do
deleteSocketFileIfExist socketFilePath
sock <- socket AF_UNIX Stream defaultProtocol
bind sock $ SockAddrUnix socketFilePath
mapM_ (setFileMode socketFilePath) maybeSocketFileMode
return sock
where
deleteSocketFileIfExist path = removeFile path `catch` handleDoesNotExist
handleDoesNotExist e
| isDoesNotExistError e = return ()
| otherwise = throwIO e
-- run the postgrest application with user defined socket.
runAppInSocket :: Settings -> Application -> Either Text FileMode -> FilePath -> IO ()
runAppInSocket settings app socketFileMode sockPath = do
sock <- createAndBindSocket sockPath (rightToMaybe socketFileMode)
putStrLn $ ("Listening on unix socket " :: Text) <> show sockPath
listen sock maxListenQueue
runSettingsSocket settings sock app
-- clean socket up when done
close sock
+163
View File
@@ -0,0 +1,163 @@
# Nix development and build environment
With Nix it's possible to quickly and reliably recreate the full environments
for developing, testing and building PostgREST.
## Getting started with Nix
You'll need to [get Nix](https://nixos.org/download.html). The installer will
create your Nix store in the `/nix/` directory, where all build artifacts and
their dependencies will be stored. It will also link the Nix executables like
`nix-env`, `nix-build` and `nix-shell` into your PATH. Nix will manage all
other PostgREST dependencies from here on out. To clean up older build
artifacts from the `/nix/store`, you can run `nix-collect-garbage`.
## Building PostgREST
To build PostgREST from your local checkout of the repository, run:
```bash
nix-build --attr postgrest
```
This will create a `result` directory that contains the PostgREST binary at
`result/bin/postgrest`. The `--attr` parameter (or short: `-A`) tells Nix to
build the `postgrest` attribute from the Nix expression it finds in our
`default.nix` (see below for details). Nix will take care of getting the right
GHC version and all the build dependencies.
## Developing
A development environment for PostgREST is available with `nix-shell`. The
following command will put you into a new shell that has GHC and Cabal on the
PATH:
```bash
nix-shell
```
Within `nix-shell`, you can run Cabal commands as usual. You can also run
stack with the `--nix` option, which causes stack to pick up the non-Haskell
dependencies from the same pinned Nixpkgs version that the Nix builds use.
## Aside: Working with `nix-shell` and the PostgREST utility scripts
The PostgREST utilities available in `nix-shell` all have names that begin
with `postgrest-`, so you can use tab completion (`postgrest-<tab>`) in
`nix-shell` to see all that are available:
```
# Note: The utilities listed here might not be up to date.
[nix-shell:~/Projects/postgrest]$ postgrest-
postgrest-lint postgrest-test-spec-postgresql-10.12
postgrest-style postgrest-test-spec-postgresql-11.7
postgrest-style-check postgrest-test-spec-postgresql-12.2
postgrest-test-all postgrest-test-spec-postgresql-9.4.24
postgrest-test-spec postgrest-test-spec-postgresql-9.5.21
postgrest-test-spec-all postgrest-test-spec-postgresql-9.6.17
[nix-shell:~/Projects/postgrest]$
```
To run one-off commands, you can also use `nix-shell --run <command>`, which
will lauch the Nix shell, run that one command and exit. Note that the tab
completion will not work with `nix-shell --run`, as Nix has yet to evaluate
our Nix expressions to see which utilities are available.
```
$ nix-shell --run postgrest-style
# Note that you need to quote any arguments that you would like to pass to
# the command to be run in nix-shell:
$ nix-shell --run "postgrest-foo --bar"
```
A third option is to install utilities that you use very often locally:
```
$ nix-env -f default.nix -iA style
# `postgrest-style` can now be run directly:
$ postgrest-style
```
Note that this does not yet work for all utilities (e.g. `postgrest-test-spec` currently
needs to be run within the Nix shell environment).
If you use `nix-shell` very often, you might like to use https://github.com/xzfc/cached-nix-shell,
which skips evaluating all our Nix expressions if nothing changed, reducing startup time for the
shell considerably.
## Testing
In nix-shell, you'll find utility scripts that make it very easy to run the
Haskell test suite, including setting up all required dependencies and
temporary test databases:
```bash
# Run the tests against the most recent version of PostgreSQL:
$ nix-shell --run postgrest-test-spec
# Run the tests against all supported versions of PostgreSQL:
$ nix-shell --run postgrest-test-spec-all
# Run the tests against a specific version of PostgreSQL (use tab-completion in
# nix-shell to see all available versions):
$ nix-shell --run postgrest-test-spec-postgresql-9.5.21
```
## Linting and styling code
The nix-shell also contains scripts for linting and styling the PostgREST
source code:
```bash
# Linting
$ nix-shell --run postgrest-lint
# Styling / auto-formatting code
$ nix-shell --run postgrest-style
```
## Tour
The following is not required for working on PostgREST with Nix, but it will
give you some more background and details on how it works.
### `default.nix`
[`default.nix`](../default.nix) is our 'respository expression' that pulls all
the pieces that we define with Nix together. It returns a set (like a dict in
other programming languages), where each attribute is a derivation that Nix
knows how to build, like the `postgrest` attribute from earlier.
Internally, our `default.nix` uses the `pkgs.callPackage` function to import
the modules that we defined in the `nix` directory. It automatically passes the
arguments those modules require if they are available in `pkgs` (this means
that `pkgs` is defined in terms of itself, better not to think too much about
that).
We also use `default.nix` to load our pinned version of the `nixpkgs`
repository. This set of packages will always be the same, independently from
where or when you use it. The pinned version can be upgraded with the small
`nixpkgs-upgrade` utility. Running `nixpkgs-upgrade > nix/nixpkgs-version.nix`
in `nix-shell` will upgrade the pinned version to the latest `nixpkgs-unstable`
version.
### `shell.nix`
[`shell.nix`](../shell.nix) defines an environment in which PostgREST can be
built and developed. It extends the build enviroment from our `postgrest`
attribute with useful utilities that will be put on the PATH in `nix-shell`.
### `nix/overlays`
Our overlays to the Nix package set are defined here. They allow us to tweak our
`pkgs` in `default.nix` by adding new packages or overriding existing ones.
+63
View File
@@ -0,0 +1,63 @@
{ postgrest, dockerTools, writeShellScriptBin }:
let
image =
tag:
dockerTools.buildImage {
inherit tag;
name = "postgrest/postgrest";
contents = postgrest;
# Set the current time as the image creation date. This makes the build
# non-reproducible, but that should not be an issue for us.
created = "now";
extraCommands =
''
mkdir etc
cp ${./postgrest.conf} etc/postgrest.conf
'';
config = {
Cmd = [ "/bin/postgrest" "/etc/postgrest.conf" ];
Env = [
"PGRST_DB_URI=postgresql://?user=postgres"
"PGRST_DB_SCHEMA=public"
"PGRST_DB_ANON_ROLE="
"PGRST_DB_POOL=100"
"PGRST_DB_EXTRA_SEARCH_PATH=public"
"PGRST_SERVER_HOST=*4"
"PGRST_SERVER_PORT=3000"
"PGRST_OPENAPI_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"
"PGRST_ROOT_SPEC="
"PGRST_RAW_MEDIA_TYPES="
];
User = "1000";
ExposedPorts = {
"3000/tcp" = {};
};
};
};
in
rec {
imageLatest =
image "latest";
imageWithVersion =
image "v${postgrest.version}";
load =
writeShellScriptBin "postgrest-docker-load"
''
set -euo pipefail
docker load -i ${imageLatest}
docker load -i ${imageWithVersion}
'';
}
+19
View File
@@ -0,0 +1,19 @@
db-uri = "$(PGRST_DB_URI)"
db-schema = "$(PGRST_DB_SCHEMA)"
db-anon-role = "$(PGRST_DB_ANON_ROLE)"
db-pool = "$(PGRST_DB_POOL)"
db-extra-search-path = "$(PGRST_DB_EXTRA_SEARCH_PATH)"
server-host = "$(PGRST_SERVER_HOST)"
server-port = "$(PGRST_SERVER_PORT)"
openapi-server-proxy-uri = "$(PGRST_OPENAPI_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)"
root-spec = "$(PGRST_ROOT_SPEC)"
raw-media-types = "$(PGRST_RAW_MEDIA_TYPES)"
+12
View File
@@ -0,0 +1,12 @@
{ writeShellScriptBin, git, silver-searcher, hlint }:
writeShellScriptBin "postgrest-lint"
''
set -euo pipefail
rootdir="$(${git}/bin/git rev-parse --show-toplevel)"
# Lint Haskell files
${silver-searcher}/bin/ag -l -g '\.l?hs$' "$rootdir" \
| xargs ${hlint}/bin/hlint -X QuasiQuotes -X NoPatternSynonyms
''
+41
View File
@@ -0,0 +1,41 @@
{ curl
, jq
, writeShellScriptBin
, nix
}:
# Utility script for pinning the latest unstable version of Nixpkgs.
# Instead of pinning Nixpkgs based on the huge Git repository, we reference a
# specific tarball that only contains the source of the revision that we want
# to pin.
let
name =
"nixpkgs-upgrade";
refUrl =
https://api.github.com/repos/nixos/nixpkgs/git/ref/heads/nixpkgs-unstable;
githubV3Header =
"Accept: application/vnd.github.v3+json";
tarballUrlBase =
https://github.com/nixos/nixpkgs/archive/;
in
writeShellScriptBin name
''
set -euo pipefail
commitHash="$(${curl}/bin/curl "${refUrl}" -H "${githubV3Header}" | ${jq}/bin/jq -r .object.sha)"
tarballUrl="${tarballUrlBase}$commitHash.tar.gz"
tarballHash="$(${nix}/bin/nix-prefetch-url --unpack "$tarballUrl")"
currentDate="$(date --iso)"
cat << EOF
# Pinned version of Nixpkgs, generated with ${name}.
{
date = "$currentDate";
rev = "$commitHash";
tarballHash = "$tarballHash";
}
EOF
''
+6
View File
@@ -0,0 +1,6 @@
# Pinned version of Nixpkgs, generated with nixpkgs-upgrade.
{
date = "2020-04-19";
rev = "10100a97c8964e82b30f180fda41ade8e6f69e41";
tarballHash = "011f36kr3c1ria7rag7px26bh73d1b0xpqadd149bysf4hg17rln";
}
+19
View File
@@ -0,0 +1,19 @@
self: super:
# Overlay that adds the `gitignoreSource` function from Hercules-CI.
# This function is useful for filtering which files are added to the Nix store.
# See: https://github.com/hercules-ci/gitignore.nix
# To update to a newer revision, the simplest way is to add a new commit hash
# from GitHub under `rev` and to then add the hash that Nix suggests on first
# use.
{
gitignoreSource =
let
gitignoreSrc = super.fetchFromGitHub {
owner = "hercules-ci";
repo = "gitignore";
rev = "2ced4519f865341adcb143c5d668f955a2cb997f";
sha256 = "sha256:0fc5bgv9syfcblp23y05kkfnpgh3gssz6vn24frs8dzw39algk2z";
};
in (super.callPackage gitignoreSrc {}).gitignoreSource;
}
+10
View File
@@ -0,0 +1,10 @@
The `.nix` files in this directory pin specific versions of Haskell packages
from Hackage. They were generated with `cabal2nix`, e.g.:
```bash
cabal2nix cabal://protolude > protolude.nix
```
Those overrides might become obsolete as the pinned versions are adopted into
our pinned version of nixpkgs.
+11
View File
@@ -0,0 +1,11 @@
self: super:
{
haskellPackages =
super.haskellPackages.override {
overrides =
final: prev: rec {
protolude = prev.callPackage ./protolude.nix {};
};
};
}
@@ -0,0 +1,43 @@
{ mkDerivation
, array
, async
, base
, bytestring
, containers
, deepseq
, ghc-prim
, hashable
, mtl
, mtl-compat
, stdenv
, stm
, text
, transformers
, transformers-compat
}:
mkDerivation {
pname = "protolude";
version = "0.3.0";
sha256 = "4083385a9e03fab9201f63ce198b9ced3fbc1c50d6d42574db5e36c757bedcac";
isLibrary = true;
isExecutable = true;
libraryHaskellDepends = [
array
async
base
bytestring
containers
deepseq
ghc-prim
hashable
mtl
mtl-compat
stm
text
transformers
transformers-compat
];
homepage = "https://github.com/sdiehl/protolude";
description = "A small prelude";
license = stdenv.lib.licenses.mit;
}
+20
View File
@@ -0,0 +1,20 @@
self: super:
# Overlay that adds legacy versions of PostgreSQL that are supported by
# PostgREST.
{
# PostgreSQL 9.4 was removed from Nixpkgs with
# https://github.com/NixOS/nixpkgs/commit/8e2fc57a80d761c46702c3250e61c1bffe021e25
# We pin its parent commit 3b5b9a7 to get the last version that was available.
postgresql_9_4 =
let
rev = "3b5b9a73f59ff93d50156e250203410bdd07f4e0";
tarballHash = "0kr25xqbv1ldp72jbmml21pc5hl7xcfqhclv5qxa5f860jddjznk";
pinnedPkgs =
builtins.fetchTarball {
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
sha256 = tarballHash;
};
in
(import pinnedPkgs {}).pkgs.postgresql_9_4;
}
+25
View File
@@ -0,0 +1,25 @@
{ runCommand }:
{
# Apply patches to a directory.
applyPatches =
name: path: patches:
runCommand name { inherit patches; }
''
set -eou pipefail
cp -r ${path} $out
chmod -R u+w $out
for patch in $patches; do
echo "Applying patch $patch"
patch -d "$out" -p1 < "$patch"
done
'';
static-haskell-nix-postgrest-libpq =
./static-haskell-nix-postgrest-libpq.patch;
nixpkgs-revert-ghc-bootstrap =
./nixpkgs-revert-ghc-bootstrap.patch;
}
@@ -0,0 +1,125 @@
From 93cc144b215654ca9cb0d2bcb58bb8922895ccab Mon Sep 17 00:00:00 2001
From: Remo <remo>
Date: Sat, 25 Apr 2020 17:31:55 +0200
Subject: [PATCH] Revert "ghc: 8.6.3-binary -> 8.6.5-binary"
This reverts commit 3c7ef6bcd85e3e133ac6f2ab5e238934d56d902e.
---
.../{8.6.5-binary.nix => 8.6.3-binary.nix} | 13 ++++++------
pkgs/top-level/haskell-packages.nix | 20 +++++++++----------
2 files changed, 16 insertions(+), 17 deletions(-)
rename pkgs/development/compilers/ghc/{8.6.5-binary.nix => 8.6.3-binary.nix} (95%)
diff --git a/pkgs/development/compilers/ghc/8.6.5-binary.nix b/pkgs/development/compilers/ghc/8.6.3-binary.nix
similarity index 95%
rename from pkgs/development/compilers/ghc/8.6.5-binary.nix
rename to pkgs/development/compilers/ghc/8.6.3-binary.nix
index 97793d912895..9e0fd12b96cf 100644
--- a/pkgs/development/compilers/ghc/8.6.5-binary.nix
+++ b/pkgs/development/compilers/ghc/8.6.3-binary.nix
@@ -27,19 +27,18 @@ let
in
stdenv.mkDerivation rec {
- version = "8.6.5";
+ version = "8.6.3";
name = "ghc-${version}-binary";
- # https://downloads.haskell.org/~ghc/8.6.5/
src = fetchurl ({
i686-linux = {
- url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-i386-deb9-linux.tar.xz";
- sha256 = "1p2h29qghql19ajk755xa0yxkn85slbds8m9n5196ris743vkp8w";
+ url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-i386-deb8-linux.tar.xz";
+ sha256 = "0bw8a7fxcbskf93rb4m542ff66vrmx5i5kj77qx37cbhijx70w5m";
};
x86_64-linux = {
- url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-x86_64-deb9-linux.tar.xz";
- sha256 = "1pqlx6rdjs2110g0y1i9f8x18lmdizibjqd15f5xahcz39hgaxdw";
+ url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-x86_64-deb8-linux.tar.xz";
+ sha256 = "1m9gaga2pzi2cx5gvasg0rx1dlvr68gmi20l67652kag6xjsa719";
};
aarch64-linux = {
url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-aarch64-ubuntu18.04-linux.tar.xz";
@@ -47,7 +46,7 @@ stdenv.mkDerivation rec {
};
x86_64-darwin = {
url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-x86_64-apple-darwin.tar.xz";
- sha256 = "0s9188vhhgf23q3rjarwhbr524z6h2qga5xaaa2pma03sfqvvhfz";
+ sha256 = "1hbzk57v45176kxcx848p5jn5p1xbp2129ramkbzsk6plyhnkl3r";
};
}.${stdenv.hostPlatform.system}
or (throw "cannot bootstrap GHC on this platform"));
diff --git a/pkgs/top-level/haskell-packages.nix b/pkgs/top-level/haskell-packages.nix
index aa7ef02a3886..712a98e147e0 100644
--- a/pkgs/top-level/haskell-packages.nix
+++ b/pkgs/top-level/haskell-packages.nix
@@ -4,7 +4,7 @@ let
# These are attributes in compiler and packages that don't support integer-simple.
integerSimpleExcludes = [
"ghc822Binary"
- "ghc865Binary"
+ "ghc863Binary"
"ghc844"
"ghcjs"
"ghcjs86"
@@ -42,7 +42,7 @@ in {
ghc822Binary = callPackage ../development/compilers/ghc/8.2.2-binary.nix { };
- ghc865Binary = callPackage ../development/compilers/ghc/8.6.5-binary.nix { };
+ ghc863Binary = callPackage ../development/compilers/ghc/8.6.3-binary.nix { };
ghc844 = callPackage ../development/compilers/ghc/8.4.4.nix {
bootPkgs = packages.ghc822Binary;
@@ -57,31 +57,31 @@ in {
llvmPackages = pkgs.llvmPackages_6;
};
ghc881 = callPackage ../development/compilers/ghc/8.8.1.nix {
- bootPkgs = packages.ghc865Binary;
+ bootPkgs = packages.ghc863Binary;
inherit (buildPackages.python3Packages) sphinx;
buildLlvmPackages = buildPackages.llvmPackages_7;
llvmPackages = pkgs.llvmPackages_7;
};
ghc882 = callPackage ../development/compilers/ghc/8.8.2.nix {
- bootPkgs = packages.ghc865Binary;
+ bootPkgs = packages.ghc863Binary;
inherit (buildPackages.python3Packages) sphinx;
buildLlvmPackages = buildPackages.llvmPackages_7;
llvmPackages = pkgs.llvmPackages_7;
};
ghc883 = callPackage ../development/compilers/ghc/8.8.3.nix {
- bootPkgs = packages.ghc865Binary;
+ bootPkgs = packages.ghc863Binary;
inherit (buildPackages.python3Packages) sphinx;
buildLlvmPackages = buildPackages.llvmPackages_7;
llvmPackages = pkgs.llvmPackages_7;
};
ghc8101 = callPackage ../development/compilers/ghc/8.10.1.nix {
- bootPkgs = packages.ghc865Binary;
+ bootPkgs = packages.ghc863Binary;
inherit (buildPackages.python3Packages) sphinx;
buildLlvmPackages = buildPackages.llvmPackages_9;
llvmPackages = pkgs.llvmPackages_9;
};
ghcHEAD = callPackage ../development/compilers/ghc/head.nix {
- bootPkgs = packages.ghc883; # no binary yet
+ bootPkgs = packages.ghc863Binary;
inherit (buildPackages.python3Packages) sphinx;
buildLlvmPackages = buildPackages.llvmPackages_9;
llvmPackages = pkgs.llvmPackages_9;
@@ -118,9 +118,9 @@ in {
compilerConfig = callPackage ../development/haskell-modules/configuration-ghc-8.2.x.nix { };
packageSetConfig = bootstrapPackageSet;
};
- ghc865Binary = callPackage ../development/haskell-modules {
- buildHaskellPackages = bh.packages.ghc865Binary;
- ghc = bh.compiler.ghc865Binary;
+ ghc863Binary = callPackage ../development/haskell-modules {
+ buildHaskellPackages = bh.packages.ghc863Binary;
+ ghc = bh.compiler.ghc863Binary;
compilerConfig = callPackage ../development/haskell-modules/configuration-ghc-8.6.x.nix { };
packageSetConfig = bootstrapPackageSet;
};
@@ -0,0 +1,25 @@
From bb4c1e27e391eff01591fe60830ff68a9ada41ef Mon Sep 17 00:00:00 2001
From: Remo <remo>
Date: Wed, 22 Apr 2020 11:58:07 +0200
Subject: [PATCH] Add postgrest libpq linking fix
---
survey/default.nix | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/survey/default.nix b/survey/default.nix
index b28606c..fd7cde7 100644
--- a/survey/default.nix
+++ b/survey/default.nix
@@ -1052,6 +1052,11 @@ let
super.squeal-postgresql
[ final.openssl ]
"--libs openssl";
+ postgrest =
+ addStaticLinkerFlagsWithPkgconfig
+ super.postgrest
+ [ final.openssl ]
+ "--libs openssl";
xml-to-json =
addStaticLinkerFlagsWithPkgconfig
+90
View File
@@ -0,0 +1,90 @@
{ mkDerivation
, array
, base
, base-compat
, base-orphans
, binary
, bytestring
, containers
, deepseq
, Diff
, directory
, filepath
, integer-logarithms
, mtl
, optparse-applicative
, parsec
, pretty
, process
, QuickCheck
, stdenv
, stm
, tagged
, tar
, tasty
, tasty-golden
, tasty-hunit
, tasty-quickcheck
, temporary
, text
, time
, transformers
, tree-diff
, unix
}:
mkDerivation {
pname = "Cabal";
version = "3.0.0.0";
sha256 = "5143ec26d740c1a508c93a8860e64407e7546c29b9817db20ff1595c1968d287";
setupHaskellDepends = [ mtl parsec ];
libraryHaskellDepends = [
array
base
binary
bytestring
containers
deepseq
directory
filepath
mtl
parsec
pretty
process
text
time
transformers
unix
];
testHaskellDepends = [
array
base
base-compat
base-orphans
binary
bytestring
containers
deepseq
Diff
directory
filepath
integer-logarithms
optparse-applicative
pretty
process
QuickCheck
stm
tagged
tar
tasty
tasty-golden
tasty-hunit
tasty-quickcheck
temporary
text
tree-diff
];
doCheck = false;
homepage = "http://www.haskell.org/cabal/";
description = "A framework for packaging Haskell software";
license = stdenv.lib.licenses.bsd3;
}
+89
View File
@@ -0,0 +1,89 @@
# Turn a Haskell source package (given its name and source) into a derivation
# for a fully static executable.
{ nixpkgs, compiler, name, src, patches }:
let
# The nh2/static-haskell-nix project does all the hard work for us for
# building static Haskell executables. We apply a patch for PostgREST below
# until the respective pull request is merged. See:
# https://github.com/nh2/static-haskell-nix/pull/91
# statix-haskell-nix builds everything based on Musl instead of glibc, so
# there will be a _lot_ to rebuild if you don't use a binary cache.
static-haskell-nix =
let
rev = "749707fc90b781c3e653e67917a7d571fe82ae7b";
in
builtins.fetchTarball {
url = "https://github.com/nh2/static-haskell-nix/archive/${rev}.tar.gz";
sha256 = "155spda2lww378bhx68w6dxwqd5y6s9kin3qbgl2m23r3vmk3m3w";
};
patched-static-haskell-nix =
patches.applyPatches "patched-static-haskell-nix" static-haskell-nix
[
patches.static-haskell-nix-postgrest-libpq
];
haskellPackagesOverrides =
lib: final: prev:
{
# Add our source package.
"${name}" = prev.callCabal2nix name src {};
# cabal2nix depends on Cabal 3.0.*, while our pinned version of Nixpkgs
# only provides 2.4 or 3.2. So we pinned 3.0.0.0 in ./Cabal.nix
cabal2nix =
prev.cabal2nix.overrideScope
(self: super: { Cabal = self.callPackage ./Cabal.nix {}; });
# Pinned version of protolude.
protolude =
prev.callPackage ../overlays/haskell-packages/protolude.nix {};
# The tests for the packages below took a long time on static
# builds, so we disable them for now - to be investigated.
happy = lib.dontCheck prev.happy;
};
# This overlay adds our source package and applies adjustments to the
# derivation of other packages that it depends on. The overlay applies to
# Haskell packages for the given compiler, which we will later use
# with the static-haskell-nix survey.
overlay =
self: super:
# Override the set of Haskell packages at
# pkgs.haskell.packages."${compiler}".
{
haskell = super.haskell // {
packages = super.haskell.packages // {
"${compiler}" =
super.haskell.packages."${compiler}".override
{ overrides = haskellPackagesOverrides self.haskell.lib; };
};
};
};
patchedNixpkgs =
patches.applyPatches "patched-nixpkgs" nixpkgs
[
patches.nixpkgs-revert-ghc-bootstrap
];
# Apply our overlay to the given pkgs.
normalPkgs =
(import patchedNixpkgs {}).appendOverlays [ overlay ];
# Each version of GHC needs a specific version of Cabal.
defaultCabalPackageVersionComingWithGhc =
{
ghc883 = "Cabal_3_2_0_0";
}."${compiler}";
# Let the static-haskell-nix project do the hard work of deriving a set of
# fully static Haskell executables, including one for the our source package
# that we added through the overlay.
survey =
import "${patched-static-haskell-nix}/survey"
{ inherit normalPkgs compiler defaultCabalPackageVersionComingWithGhc; };
in
# Return the fully static derivation of our source package.
survey.haskellPackages."${name}"
+37
View File
@@ -0,0 +1,37 @@
{ writeShellScriptBin
, git
, nixpkgs-fmt
, silver-searcher
, stylish-haskell
, buildEnv
}:
let
style =
writeShellScriptBin "postgrest-style"
''
set -euo pipefail
rootdir="$(${git}/bin/git rev-parse --show-toplevel)"
# Format Nix files
${nixpkgs-fmt}/bin/nixpkgs-fmt "$rootdir" > /dev/null 2> /dev/null
# Format Haskell files
${silver-searcher}/bin/ag -l -g '\.l?hs$' "$rootdir" \
| xargs ${stylish-haskell}/bin/stylish-haskell -i
'';
check =
writeShellScriptBin "postgrest-style-check"
''
set -euo pipefail
${style}/bin/${style.name}
${git}/bin/git diff-index --exit-code HEAD -- '*.hs' '*.lhs' '*.nix'
'';
in
buildEnv {
name = "postgrest-style";
paths = [ style check ];
}
+90
View File
@@ -0,0 +1,90 @@
# Utilities for running the PostgREST test suite
{ buildEnv
, cabal-install
, git
, lib
, postgrestBuildEnv
, postgresqlVersions
, runtimeShell
, writeShellScript
, writeShellScriptBin
}:
let
# Wrap the `test/with_tmp_db` script with the required dependencies from Nix.
withTmpDb =
postgresql:
writeShellScript "postgrest-test-${postgresql.name}"
''
set -euo pipefail
export PATH=${postgresql}/bin:${git}/bin:${runtimeShell}/bin:"$PATH"
exec ${../test/with_tmp_db} "$@"
'';
# Script to run the Haskell test suite against a specific version of
# PostgreSQL.
testSpec =
name: postgresql:
writeShellScriptBin name
''
set -euo pipefail
cat << EOF
Running spec against ${postgresql.name}...
EOF
# TODO: Make this work outside nix-shell when installed with nix-env.
# Probably using postgrestBuildEnv somehow?
${withTmpDb postgresql} ${cabal-install}/bin/cabal v2-test \
--test-show-detail=direct
cat << EOF
Done running spec against ${postgresql.name}.
EOF
'';
# The PostgreSQL version that we run the tests against by default.
defaultPostgresql =
builtins.head postgresqlVersions;
defaultTestSpec =
testSpec "postgrest-test-spec" defaultPostgresql;
# Create a `testSpec` for each PostgreSQL version that we want to test
# against.
testSpecVersions =
map
(postgresql: testSpec "postgrest-test-spec-${postgresql.name}" postgresql)
postgresqlVersions;
# Helper script for running the tests against all PostgreSQL versions.
testSpecAllVersions =
let
testRunners =
map (test: "${test}/bin/${test.name}") testSpecVersions;
in
writeShellScriptBin "postgrest-test-spec-all"
''
set -euo pipefail
${lib.concatStringsSep "\n" testRunners}
'';
in
# Create an environment that contains all the utility scripts for running tests
# that we defined above.
buildEnv {
name =
"postgrest-tests";
paths =
[
defaultTestSpec
testSpecAllVersions
] ++ testSpecVersions;
}
+91 -39
View File
@@ -1,13 +1,13 @@
name: postgrest name: postgrest
version: 6.0.2 version: 7.0.1
synopsis: REST API for any Postgres database synopsis: REST API for any Postgres database
description: Reads the schema of a PostgreSQL database and creates RESTful routes description: Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security for the tables and views, supporting all HTTP verbs that security
permits. permits.
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Joe Nelson, Adam Baker author: Joe Nelson, Adam Baker, Steve Chavez
maintainer: Steve Chávez <stevechavezast@gmail.com> maintainer: Steve Chavez <stevechavezast@gmail.com>
category: Executable, PostgreSQL, Network APIs category: Executable, PostgreSQL, Network APIs
homepage: https://postgrest.org homepage: https://postgrest.org
bug-reports: https://github.com/PostgREST/postgrest/issues bug-reports: https://github.com/PostgREST/postgrest/issues
@@ -36,24 +36,23 @@ library
PostgREST.OpenAPI PostgREST.OpenAPI
PostgREST.Parsers PostgREST.Parsers
PostgREST.QueryBuilder PostgREST.QueryBuilder
PostgREST.Statements
PostgREST.RangeQuery PostgREST.RangeQuery
PostgREST.Types PostgREST.Types
other-modules: Paths_postgrest other-modules: Paths_postgrest
PostgREST.QueryBuilder.Private PostgREST.Private.Common
PostgREST.QueryBuilder.Procedure PostgREST.Private.QueryFragment
PostgREST.QueryBuilder.ReadStatement
PostgREST.QueryBuilder.WriteStatement
hs-source-dirs: src hs-source-dirs: src
build-depends: base >= 4.9 && < 4.13 build-depends: base >= 4.9 && < 4.15
, HTTP >= 4000.3.7 && < 4000.4 , HTTP >= 4000.3.7 && < 4000.4
, Ranged-sets >= 0.3 && < 0.5 , Ranged-sets >= 0.3 && < 0.5
, aeson >= 0.11.3 && < 1.5 , aeson >= 1.4.7 && < 1.5
, ansi-wl-pprint >= 0.6.7 && < 0.7 , ansi-wl-pprint >= 0.6.7 && < 0.7
, base64-bytestring >= 1 && < 1.1 , base64-bytestring >= 1 && < 1.2
, bytestring >= 0.10.8 && < 0.11 , bytestring >= 0.10.8 && < 0.11
, case-insensitive >= 1.2 && < 1.3 , case-insensitive >= 1.2 && < 1.3
, cassava >= 0.4.5 && < 0.6 , cassava >= 0.4.5 && < 0.6
, configurator-pg >= 0.1 && < 0.2 , configurator-pg >= 0.2 && < 0.3
, containers >= 0.5.7 && < 0.7 , containers >= 0.5.7 && < 0.7
, contravariant >= 1.4 && < 1.6 , contravariant >= 1.4 && < 1.6
, contravariant-extras >= 0.3.3 && < 0.4 , contravariant-extras >= 0.3.3 && < 0.4
@@ -62,23 +61,23 @@ library
, gitrev >= 1.2 && < 1.4 , gitrev >= 1.2 && < 1.4
, hasql >= 1.4 && < 1.5 , hasql >= 1.4 && < 1.5
, hasql-pool >= 0.5 && < 0.6 , hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7.2 && < 0.8 , hasql-transaction >= 0.7.2 && < 1.1
, heredoc >= 0.2 && < 0.3 , heredoc >= 0.2 && < 0.3
, http-types >= 0.12.2 && < 0.13 , http-types >= 0.12.2 && < 0.13
, insert-ordered-containers >= 0.1 && < 0.3 , insert-ordered-containers >= 0.2.2 && < 0.3
, interpolatedstring-perl6 >= 1 && < 1.1 , interpolatedstring-perl6 >= 1 && < 1.1
, jose >= 0.8.1 && < 0.9 , jose >= 0.8.1 && < 0.9
, lens >= 4.14 && < 4.18 , lens >= 4.14 && < 4.20
, lens-aeson >= 1.0.1 && < 1.1 , lens-aeson >= 1.0.1 && < 1.2
, network-uri >= 2.6.1 && < 2.7 , network-uri >= 2.6.1 && < 2.8
, optparse-applicative >= 0.13 && < 0.15 , optparse-applicative >= 0.13 && < 0.16
, parsec >= 3.1.11 && < 3.2 , parsec >= 3.1.11 && < 3.2
, protolude >= 0.2.2 && < 0.3 , protolude >= 0.3 && < 0.4
, regex-tdfa >= 1.2.2 && < 1.3 , regex-tdfa >= 1.2.2 && < 1.4
, scientific >= 0.3.4 && < 0.4 , scientific >= 0.3.4 && < 0.4
, swagger2 >= 2.1.4 && < 2.4 , swagger2 >= 2.4 && < 2.7
, text >= 1.2.2 && < 1.3 , text >= 1.2.2 && < 1.3
, time >= 1.6 && < 1.9 , time >= 1.6 && < 1.11
, unordered-containers >= 0.2.8 && < 0.3 , unordered-containers >= 0.2.8 && < 0.3
, vector >= 0.11 && < 0.13 , vector >= 0.11 && < 0.13
, wai >= 3.2.1 && < 3.3 , wai >= 3.2.1 && < 3.3
@@ -89,33 +88,45 @@ library
default-extensions: OverloadedStrings default-extensions: OverloadedStrings
QuasiQuotes QuasiQuotes
NoImplicitPrelude NoImplicitPrelude
ghc-options: -O2 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path
-- -fno-spec-constr may help keep compile time memory use in check,
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
-- -optP-Wno-nonportable-include-path
-- prevents build failures on case-insensitive filesystems (macos),
-- see https://github.com/commercialhaskell/stack/issues/3918
executable postgrest executable postgrest
main-is: Main.hs main-is: Main.hs
hs-source-dirs: main hs-source-dirs: main
build-depends: base >= 4.9 && < 4.13 build-depends: base >= 4.9 && < 4.15
, auto-update >= 0.1.4 && < 0.2 , auto-update >= 0.1.4 && < 0.2
, base64-bytestring >= 1 && < 1.1 , base64-bytestring >= 1 && < 1.2
, bytestring >= 0.10.8 && < 0.11 , bytestring >= 0.10.8 && < 0.11
, directory >= 1.2.6 && < 1.4 , directory >= 1.2.6 && < 1.4
, either >= 4.4.1 && < 5.1
, hasql >= 1.4 && < 1.5 , hasql >= 1.4 && < 1.5
, hasql-pool >= 0.5 && < 0.6 , hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7.2 && < 0.8 , hasql-transaction >= 0.7.2 && < 1.1
, network < 2.9 , network < 3.2
, postgrest , postgrest
, protolude >= 0.2.2 && < 0.3 , protolude >= 0.3 && < 0.4
, retry >= 0.7.4 && < 0.9 , retry >= 0.7.4 && < 0.9
, text >= 1.2.2 && < 1.3 , text >= 1.2.2 && < 1.3
, time >= 1.6 && < 1.9 , time >= 1.6 && < 1.11
, warp >= 3.2.12 && < 3.3 , wai >= 3.2.1 && < 3.3
, warp >= 3.2.12 && < 3.4
default-language: Haskell2010 default-language: Haskell2010
default-extensions: OverloadedStrings default-extensions: OverloadedStrings
QuasiQuotes QuasiQuotes
NoImplicitPrelude NoImplicitPrelude
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I2" ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I2"
-O2 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path
if !os(windows) if !os(windows)
build-depends: unix build-depends: unix
other-modules: UnixSocket
test-suite spec test-suite spec
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
@@ -128,6 +139,7 @@ test-suite spec
Feature.ConcurrentSpec Feature.ConcurrentSpec
Feature.CorsSpec Feature.CorsSpec
Feature.DeleteSpec Feature.DeleteSpec
Feature.EmbedDisambiguationSpec
Feature.ExtraSearchPathSpec Feature.ExtraSearchPathSpec
Feature.InsertSpec Feature.InsertSpec
Feature.JsonOperatorSpec Feature.JsonOperatorSpec
@@ -147,15 +159,16 @@ test-suite spec
Feature.UpsertSpec Feature.UpsertSpec
Feature.RawOutputTypesSpec Feature.RawOutputTypesSpec
Feature.HtmlRawOutputSpec Feature.HtmlRawOutputSpec
Feature.MultipleSchemaSpec
SpecHelper SpecHelper
TestTypes TestTypes
hs-source-dirs: test hs-source-dirs: test
build-depends: base >= 4.9 && < 4.13 build-depends: base >= 4.9 && < 4.15
, aeson >= 0.11.3 && < 1.5 , aeson >= 1.4.7 && < 1.5
, aeson-qq >= 0.8.1 && < 0.9 , aeson-qq >= 0.8.1 && < 0.9
, async >= 2.1.1 && < 2.3 , async >= 2.1.1 && < 2.3
, auto-update >= 0.1.4 && < 0.2 , auto-update >= 0.1.4 && < 0.2
, base64-bytestring >= 1 && < 1.1 , base64-bytestring >= 1 && < 1.2
, bytestring >= 0.10.8 && < 0.11 , bytestring >= 0.10.8 && < 0.11
, case-insensitive >= 1.2 && < 1.3 , case-insensitive >= 1.2 && < 1.3
, cassava >= 0.4.5 && < 0.6 , cassava >= 0.4.5 && < 0.6
@@ -163,21 +176,21 @@ test-suite spec
, contravariant >= 1.4 && < 1.6 , contravariant >= 1.4 && < 1.6
, hasql >= 1.4 && < 1.5 , hasql >= 1.4 && < 1.5
, hasql-pool >= 0.5 && < 0.6 , hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7.2 && < 0.8 , hasql-transaction >= 0.7.2 && < 1.1
, heredoc >= 0.2 && < 0.3 , heredoc >= 0.2 && < 0.3
, hspec >= 2.3 && < 2.8 , hspec >= 2.3 && < 2.8
, hspec-wai >= 0.7 && < 0.10 , hspec-wai >= 0.10 && < 0.11
, hspec-wai-json >= 0.7 && < 0.10 , hspec-wai-json >= 0.10 && < 0.11
, http-types >= 0.12.3 && < 0.13 , http-types >= 0.12.3 && < 0.13
, lens >= 4.14 && < 4.18 , lens >= 4.14 && < 4.20
, lens-aeson >= 1.0.1 && < 1.1 , lens-aeson >= 1.0.1 && < 1.2
, monad-control >= 1.0.1 && < 1.1 , monad-control >= 1.0.1 && < 1.1
, postgrest , postgrest
, process >= 1.4.2 && < 1.7 , process >= 1.4.2 && < 1.7
, protolude >= 0.2.2 && < 0.3 , protolude >= 0.3 && < 0.4
, regex-tdfa >= 1.2.2 && < 1.3 , regex-tdfa >= 1.2.2 && < 1.4
, text >= 1.2.2 && < 1.3 , text >= 1.2.2 && < 1.3
, time >= 1.6 && < 1.9 , time >= 1.6 && < 1.11
, transformers-base >= 0.4.4 && < 0.5 , transformers-base >= 0.4.4 && < 0.5
, wai >= 3.2.1 && < 3.3 , wai >= 3.2.1 && < 3.3
, wai-extra >= 3.0.19 && < 3.1 , wai-extra >= 3.0.19 && < 3.1
@@ -186,3 +199,42 @@ test-suite spec
QuasiQuotes QuasiQuotes
NoImplicitPrelude NoImplicitPrelude
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N
Test-Suite spec-querycost
Type: exitcode-stdio-1.0
Default-Language: Haskell2010
default-extensions: OverloadedStrings, QuasiQuotes, NoImplicitPrelude
Hs-Source-Dirs: test
Main-Is: QueryCost.hs
Other-Modules: SpecHelper
Build-Depends: base >= 4.9 && < 4.15
, aeson >= 1.4.7 && < 1.5
, aeson-qq >= 0.8.1 && < 0.9
, async >= 2.1.1 && < 2.3
, auto-update >= 0.1.4 && < 0.2
, base64-bytestring >= 1 && < 1.2
, bytestring >= 0.10.8 && < 0.11
, case-insensitive >= 1.2 && < 1.3
, cassava >= 0.4.5 && < 0.6
, containers >= 0.5.7 && < 0.7
, contravariant >= 1.4 && < 1.6
, hasql >= 1.4 && < 1.5
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7.2 && < 1.1
, heredoc >= 0.2 && < 0.3
, hspec >= 2.3 && < 2.8
, hspec-wai >= 0.10 && < 0.11
, hspec-wai-json >= 0.10 && < 0.11
, http-types >= 0.12.3 && < 0.13
, lens >= 4.14 && < 4.20
, lens-aeson >= 1.0.1 && < 1.2
, monad-control >= 1.0.1 && < 1.1
, postgrest
, process >= 1.4.2 && < 1.7
, protolude >= 0.3 && < 0.4
, regex-tdfa >= 1.2.2 && < 1.4
, text >= 1.2.2 && < 1.3
, time >= 1.6 && < 1.11
, transformers-base >= 0.4.4 && < 0.5
, wai >= 3.2.1 && < 3.3
, wai-extra >= 3.0.19 && < 3.1
+23
View File
@@ -0,0 +1,23 @@
with (import ./default.nix);
pkgs.lib.overrideDerivation env (
base: {
buildInputs =
base.buildInputs ++ [
pkgs.cabal-install
pkgs.stack
pkgs.cabal2nix
pkgs.postgresql
nixpkgsUpgrade
tests
style
lint
];
shellHook =
''
# Set our pinned version of Nixpkgs in the NIX_PATH so that
# `stack --nix` also uses that version.
NIX_PATH="nixpkgs=${nixpkgs}"
'';
}
)
+112 -79
View File
@@ -3,13 +3,14 @@ Module : PostgREST.ApiRequest
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest. Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
-} -}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
module PostgREST.ApiRequest ( module PostgREST.ApiRequest (
ApiRequest(..) ApiRequest(..)
, InvokeMethod(..)
, ContentType(..) , ContentType(..)
, Action(..) , Action(..)
, Target(..) , Target(..)
, PreferRepresentation (..)
, mutuallyAgreeable , mutuallyAgreeable
, userApiRequest , userApiRequest
) where ) where
@@ -28,6 +29,7 @@ import qualified Data.Vector as V
import Control.Arrow ((***)) import Control.Arrow ((***))
import Data.Aeson.Types (emptyArray, emptyObject) import Data.Aeson.Types (emptyArray, emptyObject)
import Data.List (last, lookup, partition) import Data.List (last, lookup, partition)
import Data.List.NonEmpty (head)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.Ranged.Ranges (Range (..), emptyRange, import Data.Ranged.Ranges (Range (..), emptyRange,
rangeIntersection) rangeIntersection)
@@ -47,24 +49,24 @@ import PostgREST.RangeQuery (NonnegRange, allRange, rangeGeq,
rangeLimit, rangeOffset, rangeRequested, rangeLimit, rangeOffset, rangeRequested,
restrictRange) restrictRange)
import PostgREST.Types import PostgREST.Types
import Protolude import Protolude hiding (head, toS)
import Protolude.Conv (toS)
type RequestBody = BL.ByteString type RequestBody = BL.ByteString
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
-- | Types of things a user wants to do to tables/views/procs -- | Types of things a user wants to do to tables/views/procs
data Action = ActionCreate | ActionRead data Action = ActionCreate | ActionRead{isHead :: Bool}
| ActionUpdate | ActionDelete | ActionUpdate | ActionDelete
| ActionInfo | ActionInvoke{isReadOnly :: Bool} | ActionSingleUpsert | ActionInvoke InvokeMethod
| ActionInspect | ActionSingleUpsert | ActionInfo | ActionInspect{isHead :: Bool}
deriving Eq deriving Eq
-- | The target db object of a user action -- | The target db object of a user action
data Target = TargetIdent QualifiedIdentifier data Target = TargetIdent QualifiedIdentifier
| TargetProc{tpQi :: QualifiedIdentifier, tpIsRootSpec :: Bool} | TargetProc{tpQi :: QualifiedIdentifier, tpIsRootSpec :: Bool}
| TargetDefaultSpec -- The default spec offered at root "/" | TargetDefaultSpec{tdsSchema :: Schema} -- The default spec offered at root "/"
| TargetUnknown [Text] | TargetUnknown [Text]
deriving Eq 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 Describes what the user wants to do. This data type is a
@@ -74,65 +76,61 @@ data PreferRepresentation = Full | HeadersOnly | None deriving Eq
if it is an action we are able to perform. if it is an action we are able to perform.
-} -}
data ApiRequest = ApiRequest { data ApiRequest = ApiRequest {
-- | Similar but not identical to HTTP verb, e.g. Create/Invoke both POST iAction :: Action -- ^ Similar but not identical to HTTP verb, e.g. Create/Invoke both POST
iAction :: Action , iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response
-- | Requested range of rows within response , iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
, iRange :: M.HashMap ByteString NonnegRange , iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
-- | The target, be it calling a proc or accessing a table , iAccepts :: [ContentType] -- ^ Content types the client will accept, [CTAny] if no Accept header
, iTarget :: Target , iPayload :: Maybe PayloadJSON -- ^ Data sent by client and used for mutation actions
-- | Content types the client will accept, [CTAny] if no Accept header , iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
, iAccepts :: [ContentType] , iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
-- | Data sent by client and used for mutation actions , iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
, iPayload :: Maybe PayloadJSON , iPreferResolution :: Maybe PreferResolution -- ^ Whether the client wants to UPSERT or ignore records on PK conflict
-- | If client wants created items echoed back , iFilters :: [(Text, Text)] -- ^ Filters on the result ("id", "eq.10")
, iPreferRepresentation :: PreferRepresentation , iLogic :: [(Text, Text)] -- ^ &and and &or parameters used for complex boolean logic
-- | Pass all parameters as a single json object to a stored procedure , iSelect :: Maybe Text -- ^ &select parameter used to shape the response
, iPreferSingleObjectParameter :: Bool , iOnConflict :: Maybe Text -- ^ &on_conflict parameter used to upsert on specific unique keys
-- | Whether the client wants a result count (slower) , iColumns :: Maybe Text -- ^ &columns parameter used to shape the payload
, iPreferCount :: Bool , iOrder :: [(Text, Text)] -- ^ &order parameters for each level
-- | Whether the client wants to UPSERT or ignore records on PK conflict , iCanonicalQS :: ByteString -- ^ Alphabetized (canonical) request query string for response URLs
, iPreferResolution :: Maybe PreferResolution , iJWT :: Text -- ^ JSON Web Token
-- | Filters on the result ("id", "eq.10") , iHeaders :: [(Text, Text)] -- ^ HTTP request headers
, iFilters :: [(Text, Text)] , iCookies :: [(Text, Text)] -- ^ Request Cookies
-- | &and and &or parameters used for complex boolean logic , iPath :: ByteString -- ^ Raw request path
, iLogic :: [(Text, Text)] , iMethod :: ByteString -- ^ Raw request method
-- | &select parameter used to shape the response , iProfile :: Maybe Schema -- ^ The request profile for enabling use of multiple schemas. Follows the spec in hhttps://www.w3.org/TR/dx-prof-conneg/ttps://www.w3.org/TR/dx-prof-conneg/.
, iSelect :: Text , iSchema :: Schema -- ^ The request schema. Can vary depending on iProfile.
-- | &columns parameter used to shape the payload
, iColumns :: Maybe 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. -- | Examines HTTP request and translates it into user intent.
userApiRequest :: Schema -> Maybe QualifiedIdentifier -> Request -> RequestBody -> Either ApiRequestError ApiRequest userApiRequest :: NonEmpty Schema -> Maybe Text -> Request -> RequestBody -> Either ApiRequestError ApiRequest
userApiRequest schema rootSpec req reqBody userApiRequest confSchemas rootSpec req reqBody
| isTargetingProc && method `notElem` ["GET", "POST"] = Left ActionInappropriate | isJust profile && fromJust profile `notElem` confSchemas = Left $ UnacceptableSchema $ toList confSchemas
| isTargetingProc && method `notElem` ["HEAD", "GET", "POST"] = Left ActionInappropriate
| topLevelRange == emptyRange = Left InvalidRange | topLevelRange == emptyRange = Left InvalidRange
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload | shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
| otherwise = Right ApiRequest { | otherwise = Right ApiRequest {
iAction = action iAction = action
, iTarget = target , iTarget = target
, iRange = ranges , iRange = ranges
, iTopLevelRange = topLevelRange
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept" , iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
, iPayload = relevantPayload , iPayload = relevantPayload
, iPreferRepresentation = representation , iPreferRepresentation = representation
, iPreferSingleObjectParameter = singleObject , iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
, iPreferCount = hasPrefer "count=exact" | hasPrefer (show MultipleObjects) -> Just MultipleObjects
, iPreferResolution = if hasPrefer (show MergeDuplicates) then Just MergeDuplicates | otherwise -> Nothing
else if hasPrefer (show IgnoreDuplicates) then Just IgnoreDuplicates , iPreferCount = if | hasPrefer (show ExactCount) -> Just ExactCount
else Nothing | hasPrefer (show PlannedCount) -> Just PlannedCount
| hasPrefer (show EstimatedCount) -> Just EstimatedCount
| otherwise -> Nothing
, iPreferResolution = if | hasPrefer (show MergeDuplicates) -> Just MergeDuplicates
| hasPrefer (show IgnoreDuplicates) -> Just IgnoreDuplicates
| otherwise -> Nothing
, iFilters = filters , iFilters = filters
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ] , iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
, iSelect = toS $ fromMaybe "*" $ join $ lookup "select" qParams , iSelect = toS <$> join (lookup "select" qParams)
, iOnConflict = toS <$> join (lookup "on_conflict" qParams)
, iColumns = columns , iColumns = columns
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ] , iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
, iCanonicalQS = toS $ urlEncodeVars , iCanonicalQS = toS $ urlEncodeVars
@@ -140,8 +138,12 @@ userApiRequest schema rootSpec req reqBody
. map (join (***) toS . second (fromMaybe BS.empty)) . map (join (***) toS . second (fromMaybe BS.empty))
$ qString $ qString
, iJWT = tokenStr , iJWT = tokenStr
, iHeaders = [ (toS $ CI.foldedCase k, toS v) | (k,v) <- hdrs, k /= hAuthorization, k /= hCookie] , iHeaders = [ (toS $ CI.foldedCase k, toS v) | (k,v) <- hdrs, k /= hCookie]
, iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie" , iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie"
, iPath = rawPathInfo req
, iMethod = method
, iProfile = profile
, iSchema = schema
} }
where where
-- queryString with '+' converted to ' '(space) -- queryString with '+' converted to ' '(space)
@@ -149,8 +151,10 @@ userApiRequest schema rootSpec req reqBody
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..) -- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
(filters, rpcQParams) = (filters, rpcQParams) =
case action of case action of
ActionInvoke{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts ActionInvoke InvGet -> partitionFlts
_ -> (flts, []) ActionInvoke InvHead -> partitionFlts
_ -> (flts, [])
partitionFlts = partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
flts = flts =
[ (toS k, toS $ fromJust v) | [ (toS k, toS $ fromJust v) |
(k,v) <- qParams, isJust v, (k,v) <- qParams, isJust v,
@@ -163,13 +167,17 @@ userApiRequest schema rootSpec req reqBody
isTargetingProc = case target of isTargetingProc = case target of
TargetProc _ _ -> True TargetProc _ _ -> True
_ -> False _ -> False
isTargetingDefaultSpec = case target of
TargetDefaultSpec _ -> True
_ -> False
contentType = decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type" contentType = decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
columns | action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}] = toS <$> join (lookup "columns" qParams) columns
| otherwise = Nothing | action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
| otherwise = Nothing
payload = payload =
case (contentType, action) of case (contentType, action) of
(_, ActionInvoke{isReadOnly=True}) -> (_, ActionInvoke InvGet) -> Right rpcPrmsToJson
Right $ ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) PJObject (S.fromList $ fst <$> rpcQParams) (_, ActionInvoke InvHead) -> Right rpcPrmsToJson
(CTApplicationJSON, _) -> (CTApplicationJSON, _) ->
if isJust columns if isJust columns
then Right $ RawJSON reqBody then Right $ RawJSON reqBody
@@ -183,33 +191,57 @@ userApiRequest schema rootSpec req reqBody
(CTOther "application/x-www-form-urlencoded", _) -> (CTOther "application/x-www-form-urlencoded", _) ->
let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody
keys = S.fromList $ M.keys json in keys = S.fromList $ M.keys json in
Right $ ProcessedJSON (JSON.encode json) PJObject keys Right $ ProcessedJSON (JSON.encode json) keys
(ct, _) -> (ct, _) ->
Left $ toS $ "Content-Type not acceptable: " <> toMime ct Left $ toS $ "Content-Type not acceptable: " <> toMime ct
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges rpcPrmsToJson = ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) (S.fromList $ fst <$> rpcQParams)
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
action = action =
case method of case method of
"GET" | target == TargetDefaultSpec -> ActionInspect -- The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response
| isTargetingProc -> ActionInvoke{isReadOnly=True} -- From https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
| otherwise -> ActionRead "HEAD" | isTargetingDefaultSpec -> ActionInspect{isHead=True}
| isTargetingProc -> ActionInvoke InvHead
| otherwise -> ActionRead{isHead=True}
"GET" | isTargetingDefaultSpec -> ActionInspect{isHead=False}
| isTargetingProc -> ActionInvoke InvGet
| otherwise -> ActionRead{isHead=False}
"POST" -> if isTargetingProc "POST" -> if isTargetingProc
then ActionInvoke{isReadOnly=False} then ActionInvoke InvPost
else ActionCreate else ActionCreate
"PATCH" -> ActionUpdate "PATCH" -> ActionUpdate
"PUT" -> ActionSingleUpsert "PUT" -> ActionSingleUpsert
"DELETE" -> ActionDelete "DELETE" -> ActionDelete
"OPTIONS" -> ActionInfo "OPTIONS" -> ActionInfo
_ -> ActionInspect _ -> ActionInspect{isHead=False}
defaultSchema = head confSchemas
profile
| length confSchemas <= 1 -- only enable content negotiation by profile when there are multiple schemas specified in the config
= Nothing
| action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionDelete, ActionInvoke InvPost] -- POST/PATCH/PUT/DELETE don't use the same header as per the spec
= Just $ maybe defaultSchema toS $ lookupHeader "Content-Profile"
| action `elem` [ActionRead True, ActionRead False, ActionInvoke InvGet, ActionInvoke InvHead,
ActionInspect False, ActionInspect True, ActionInfo]
= Just $ maybe defaultSchema toS $ lookupHeader "Accept-Profile"
| otherwise = Nothing
schema = fromMaybe defaultSchema profile
target = case path of target = case path of
[] -> case rootSpec of [] -> case rootSpec of
Just rsQi -> TargetProc rsQi True Just pName -> TargetProc (QualifiedIdentifier schema pName) True
Nothing -> TargetDefaultSpec Nothing -> TargetDefaultSpec schema
[table] -> TargetIdent $ QualifiedIdentifier schema table [table] -> TargetIdent $ QualifiedIdentifier schema table
["rpc", proc] -> TargetProc (QualifiedIdentifier schema proc) False ["rpc", proc] -> TargetProc (QualifiedIdentifier schema proc) False
other -> TargetUnknown other other -> TargetUnknown other
shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionInvoke{isReadOnly=False}, ActionInvoke{isReadOnly=True}] shouldParsePayload =
action `elem`
[ActionCreate, ActionUpdate, ActionSingleUpsert,
ActionInvoke InvPost,
-- Though ActionInvoke{isGet=True}(a GET /rpc/..) doesn't really have a payload, we use the payload variable as a way
-- to store the query string arguments to the function.
ActionInvoke InvGet,
ActionInvoke InvHead]
relevantPayload | shouldParsePayload = rightToMaybe payload relevantPayload | shouldParsePayload = rightToMaybe payload
| otherwise = Nothing | otherwise = Nothing
path = pathInfo req path = pathInfo req
@@ -222,11 +254,12 @@ userApiRequest schema rootSpec req reqBody
where where
split :: BS.ByteString -> [Text] split :: BS.ByteString -> [Text]
split = map T.strip . T.split (==',') . toS split = map T.strip . T.split (==',') . toS
singleObject = hasPrefer "params=single-object"
representation representation
| hasPrefer "return=representation" = Full | hasPrefer (show Full) = Full
| hasPrefer "return=minimal" = None | hasPrefer (show None) = None
| otherwise = HeadersOnly | otherwise = if action == ActionCreate
then HeadersOnly -- Assume the user wants the Location header(for POST) by default
else None
auth = fromMaybe "" $ lookupHeader hAuthorization auth = fromMaybe "" $ lookupHeader hAuthorization
tokenStr = case T.split (== ' ') (toS auth) of tokenStr = case T.split (== ' ') (toS auth) of
("Bearer" : t : _) -> t ("Bearer" : t : _) -> t
@@ -300,14 +333,14 @@ payloadAttributes raw json =
JSON.Object x -> S.fromList (M.keys x) == canonicalKeys JSON.Object x -> S.fromList (M.keys x) == canonicalKeys
_ -> False) arr in _ -> False) arr in
if areKeysUniform if areKeysUniform
then Just $ ProcessedJSON raw (PJArray $ V.length arr) canonicalKeys then Just $ ProcessedJSON raw canonicalKeys
else Nothing else Nothing
Just _ -> Nothing Just _ -> Nothing
Nothing -> Just emptyPJArray Nothing -> Just emptyPJArray
JSON.Object o -> Just $ ProcessedJSON raw PJObject (S.fromList $ M.keys o) JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ M.keys o)
-- truncate everything else to an empty array. -- truncate everything else to an empty array.
_ -> Just emptyPJArray _ -> Just emptyPJArray
where where
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) (PJArray 0) S.empty emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
+237 -215
View File
@@ -1,4 +1,16 @@
{-|
Module : PostgREST.App
Description : PostgREST main application
This module is in charge of mapping HTTP requests to PostgreSQL queries.
Some of its functionality includes:
- Mapping HTTP request methods to proper SQL statements. For example, a GET request is translated to executing a SELECT query in a read-only TRANSACTION.
- Producing HTTP Headers according to RFCs.
- Content Negotiation
-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
@@ -15,7 +27,6 @@ import qualified Hasql.Transaction as H
import qualified Hasql.Transaction as HT import qualified Hasql.Transaction as HT
import qualified Hasql.Transaction.Sessions as HT import qualified Hasql.Transaction.Sessions as HT
import Data.Aeson as JSON
import Data.Function (id) import Data.Function (id)
import Data.IORef (IORef, readIORef) import Data.IORef (IORef, readIORef)
import Data.Time.Clock (UTCTime) import Data.Time.Clock (UTCTime)
@@ -29,29 +40,32 @@ import Network.HTTP.Types.Status
import Network.Wai import Network.Wai
import PostgREST.ApiRequest (Action (..), ApiRequest (..), import PostgREST.ApiRequest (Action (..), ApiRequest (..),
ContentType (..), InvokeMethod (..), Target (..),
PreferRepresentation (..), mutuallyAgreeable, userApiRequest)
Target (..), mutuallyAgreeable,
userApiRequest)
import PostgREST.Auth (containsRole, jwtClaims, import PostgREST.Auth (containsRole, jwtClaims,
parseSecret) parseSecret)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.DbRequestBuilder (fieldNames, mutateRequest, import PostgREST.DbRequestBuilder (mutateRequest, readRequest,
readRequest) returningCols)
import PostgREST.DbStructure import PostgREST.DbStructure
import PostgREST.Error (PgError (..), SimpleError (..), import PostgREST.Error (PgError (..), SimpleError (..),
errorResponseFor, singularityError) errorResponseFor, singularityError)
import PostgREST.Middleware import PostgREST.Middleware
import PostgREST.OpenAPI import PostgREST.OpenAPI
import PostgREST.Parsers (pRequestColumns) import PostgREST.Parsers (pRequestColumns)
import PostgREST.QueryBuilder (ResultsWithCount, callProc, import PostgREST.QueryBuilder (limitedQuery, mutateRequestToQuery,
readRequestToCountQuery,
readRequestToQuery,
requestToCallProcQuery)
import PostgREST.RangeQuery (allRange, contentRangeH,
rangeStatusHeader)
import PostgREST.Statements (callProcStatement,
createExplainStatement,
createReadStatement, createReadStatement,
createWriteStatement, createWriteStatement)
requestToCountQuery,
requestToQuery)
import PostgREST.RangeQuery (allRange, rangeOffset)
import PostgREST.Types import PostgREST.Types
import Protolude hiding (Proxy, intercalate) import Protolude hiding (Proxy, intercalate, toS)
import Protolude.Conv (toS)
postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application
postgrest conf refDbStructure pool getTime worker = postgrest conf refDbStructure pool getTime worker =
@@ -65,8 +79,9 @@ postgrest conf refDbStructure pool getTime worker =
Nothing -> respond . errorResponseFor $ ConnectionLostError Nothing -> respond . errorResponseFor $ ConnectionLostError
Just dbStructure -> do Just dbStructure -> do
response <- do response <- do
-- Need to parse ?columns early because findProc needs it to solve overloaded functions -- Need to parse ?columns early because findProc needs it to solve overloaded functions.
let apiReq = userApiRequest (configSchema conf) (configRootSpec conf) req body -- TODO: move this logic to the app function
let apiReq = userApiRequest (configSchemas conf) (configRootSpec conf) req body
apiReqCols = (,) <$> apiReq <*> (pRequestColumns =<< iColumns <$> apiReq) apiReqCols = (,) <$> apiReq <*> (pRequestColumns =<< iColumns <$> apiReq)
case apiReqCols of case apiReqCols of
Left err -> return . errorResponseFor $ err Left err -> return . errorResponseFor $ err
@@ -78,7 +93,7 @@ postgrest conf refDbStructure pool getTime worker =
(Just RawJSON{}, Just cls) -> cls (Just RawJSON{}, Just cls) -> cls
_ -> S.empty _ -> S.empty
proc = case iTarget apiRequest of proc = case iTarget apiRequest of
TargetProc qi _ -> findProc qi cols (iPreferSingleObjectParameter apiRequest) $ dbProcs dbStructure TargetProc qi _ -> findProc qi cols (iPreferParameters apiRequest == Just SingleObject) $ dbProcs dbStructure
_ -> Nothing _ -> Nothing
handleReq = runWithClaims conf eClaims (app dbStructure proc cols conf) apiRequest handleReq = runWithClaims conf eClaims (app dbStructure proc cols conf) apiRequest
txMode = transactionMode proc (iAction apiRequest) txMode = transactionMode proc (iAction apiRequest)
@@ -90,46 +105,57 @@ postgrest conf refDbStructure pool getTime worker =
transactionMode :: Maybe ProcDescription -> Action -> HT.Mode transactionMode :: Maybe ProcDescription -> Action -> HT.Mode
transactionMode proc action = transactionMode proc action =
case action of case action of
ActionRead -> HT.Read ActionRead _ -> HT.Read
ActionInfo -> HT.Read ActionInfo -> HT.Read
ActionInspect -> HT.Read ActionInspect _ -> HT.Read
ActionInvoke{isReadOnly=False} -> ActionInvoke InvGet -> HT.Read
ActionInvoke InvHead -> HT.Read
ActionInvoke InvPost ->
let v = maybe Volatile pdVolatility proc in let v = maybe Volatile pdVolatility proc in
if v == Stable || v == Immutable if v == Stable || v == Immutable
then HT.Read then HT.Read
else HT.Write else HT.Write
ActionInvoke{isReadOnly=True} -> HT.Read
_ -> HT.Write _ -> HT.Write
app :: DbStructure -> Maybe ProcDescription -> S.Set FieldName -> AppConfig -> ApiRequest -> H.Transaction Response app :: DbStructure -> Maybe ProcDescription -> S.Set FieldName -> AppConfig -> ApiRequest -> H.Transaction Response
app dbStructure proc cols conf apiRequest = app dbStructure proc cols conf apiRequest =
let rawContentTypes = (decodeContentType <$> configRawMediaTypes conf) `L.union` [ CTOctetStream, CTTextPlain ] in
case responseContentTypeOrError (iAccepts apiRequest) rawContentTypes (iAction apiRequest) (iTarget apiRequest) of case responseContentTypeOrError (iAccepts apiRequest) rawContentTypes (iAction apiRequest) (iTarget apiRequest) of
Left errorResponse -> return errorResponse Left errorResponse -> return errorResponse
Right contentType -> Right contentType ->
case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of
(ActionRead, TargetIdent qi, Nothing) -> (ActionRead headersOnly, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
let partsField = (,) <$> readSqlParts case readSqlParts tSchema tName of
<*> (binaryField contentType rawContentTypes =<< fldNames) in
case partsField of
Left errorResponse -> return errorResponse Left errorResponse -> return errorResponse
Right ((q, cq), bField) -> do Right (q, cq, bField, _) -> do
let stm = createReadStatement q cq (contentType == CTSingularJSON) shouldCount let cQuery = if estimatedCount
(contentType == CTTextCSV) bField then limitedQuery cq ((+ 1) <$> maxRows) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
else cq
stm = createReadStatement q cQuery (contentType == CTSingularJSON) shouldCount
(contentType == CTTextCSV) bField pgVer
explStm = createExplainStatement cq
row <- H.statement () stm row <- H.statement () stm
let (tableTotal, queryTotal, _ , body) = row let (tableTotal, queryTotal, _ , body, gucHeaders) = row
(status, contentRange) = rangeHeader queryTotal tableTotal case gucHeaders of
canonical = iCanonicalQS apiRequest Left _ -> return . errorResponseFor $ GucHeadersError
return $ Right ghdrs -> do
if contentType == CTSingularJSON && queryTotal /= 1 total <- if | plannedCount -> H.statement () explStm
then errorResponseFor . singularityError $ queryTotal | estimatedCount -> if tableTotal > (fromIntegral <$> maxRows)
else responseLBS status then do estTotal <- H.statement () explStm
[toHeader contentType, contentRange, pure $ if estTotal > tableTotal then estTotal else tableTotal
("Content-Location", else pure tableTotal
"/" <> toS (qiName qi) <> | otherwise -> pure tableTotal
if BS.null canonical then "" else "?" <> toS canonical let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal total
) headers = addHeadersIfNotIncluded (catMaybes [
] (toS body) Just $ toHeader contentType, Just contentRange,
Just $ contentLocationH tName (iCanonicalQS apiRequest), profileH])
(unwrapGucHeader <$> ghdrs)
rBody = if headersOnly then mempty else toS body
return $
if contentType == CTSingularJSON && queryTotal /= 1
then errorResponseFor . singularityError $ queryTotal
else responseLBS status headers rBody
(ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) -> (ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
@@ -138,32 +164,31 @@ app dbStructure proc cols conf apiRequest =
let pkCols = tablePKCols dbStructure tSchema tName let pkCols = tablePKCols dbStructure tSchema tName
stm = createWriteStatement sq mq stm = createWriteStatement sq mq
(contentType == CTSingularJSON) True (contentType == CTSingularJSON) True
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols (contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols pgVer
row <- H.statement (toS $ pjRaw pJson) stm row <- H.statement (toS $ pjRaw pJson) stm
let (_, queryTotal, fs, body) = extractQueryResult row let (_, queryTotal, fields, body, gucHeaders) = row
headers = catMaybes [ case gucHeaders of
if null fs Left _ -> return . errorResponseFor $ GucHeadersError
then Nothing Right ghdrs -> do
else Just (hLocation, "/" <> toS tName <> renderLocationFields fs) let
, if iPreferRepresentation apiRequest == Full (ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
then Just $ toHeader contentType then ([Just $ toHeader contentType, profileH], toS body)
else Nothing else ([], mempty)
, Just $ contentRangeH 1 0 $ headers = addHeadersIfNotIncluded (catMaybes ([
if shouldCount then Just queryTotal else Nothing if null fields
, if null pkCols then Nothing
then Nothing else Just $ locationH tName fields
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest , Just $ contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing
] , if null pkCols && isNothing (iOnConflict apiRequest)
if contentType == CTSingularJSON then Nothing
&& queryTotal /= 1 else (\x -> ("Preference-Applied", encodeUtf8 (show x))) <$> iPreferResolution apiRequest
&& iPreferRepresentation apiRequest == Full ] ++ ctHeaders)) (unwrapGucHeader <$> ghdrs)
then do if contentType == CTSingularJSON && queryTotal /= 1
HT.condemn then do
return . errorResponseFor . singularityError $ queryTotal HT.condemn
else return . errorResponseFor . singularityError $ queryTotal
return . responseLBS status201 headers $ else
if iPreferRepresentation apiRequest == Full return $ responseLBS status201 headers rBody
then toS body else ""
(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) -> (ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
@@ -171,62 +196,54 @@ app dbStructure proc cols conf apiRequest =
Right (sq, mq) -> do Right (sq, mq) -> do
let stm = createWriteStatement sq mq let stm = createWriteStatement sq mq
(contentType == CTSingularJSON) False (contentType == CTTextCSV) (contentType == CTSingularJSON) False (contentType == CTTextCSV)
(iPreferRepresentation apiRequest) [] (iPreferRepresentation apiRequest) [] pgVer
row <- H.statement (toS $ pjRaw pJson) stm row <- H.statement (toS $ pjRaw pJson) stm
let (_, queryTotal, _, body) = extractQueryResult row let (_, queryTotal, _, body, gucHeaders) = row
case gucHeaders of
Left _ -> return . errorResponseFor $ GucHeadersError
Right ghdrs -> do
let
updateIsNoOp = S.null cols
status | queryTotal == 0 && not updateIsNoOp = status404
| iPreferRepresentation apiRequest == Full = status200
| otherwise = status204
contentRangeHeader = contentRangeH 0 (queryTotal - 1) $ if shouldCount then Just queryTotal else Nothing
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
then ([Just $ toHeader contentType, profileH], toS body)
else ([], mempty)
headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs)
if contentType == CTSingularJSON && queryTotal /= 1
then do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
else
return $ responseLBS status headers rBody
updateIsNoOp = S.null cols (ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
contentRangeHeader = contentRangeH 0 (queryTotal - 1) $
if shouldCount then Just queryTotal else Nothing
minimalHeaders = [contentRangeHeader]
fullHeaders = toHeader contentType : minimalHeaders
status | queryTotal == 0 && not updateIsNoOp = status404
| iPreferRepresentation apiRequest == Full = status200
| otherwise = status204
case (contentType, iPreferRepresentation apiRequest) of
(CTSingularJSON, Full)
| queryTotal == 1 -> return $ responseLBS status fullHeaders (toS body)
| otherwise -> HT.condemn >> (return . errorResponseFor . singularityError) queryTotal
(_, Full) ->
return $ responseLBS status fullHeaders (toS body)
(_, _) ->
return $ responseLBS status minimalHeaders mempty
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just ProcessedJSON{pjRaw, pjType, pjKeys}) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
Left errorResponse -> return errorResponse Left errorResponse -> return errorResponse
Right (sq, mq) -> do Right (sq, mq) ->
let isSingle = case pjType of
PJArray len -> len == 1
PJObject -> True
colNames = colName <$> tableCols dbStructure tSchema tName
if topLevelRange /= allRange if topLevelRange /= allRange
then return . errorResponseFor $ PutRangeNotAllowedError then return . errorResponseFor $ PutRangeNotAllowedError
else if not isSingle
then return . errorResponseFor $ PutSingletonError
else if S.fromList colNames /= pjKeys
then return . errorResponseFor $ PutPayloadIncompleteError
else do else do
row <- H.statement (toS pjRaw) $ row <- H.statement (toS $ pjRaw pJson) $
createWriteStatement sq mq (contentType == CTSingularJSON) False createWriteStatement sq mq (contentType == CTSingularJSON) False
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) [] (contentType == CTTextCSV) (iPreferRepresentation apiRequest) [] pgVer
let (_, queryTotal, _, body) = extractQueryResult row let (_, queryTotal, _, body, gucHeaders) = row
-- Makes sure the querystring pk matches the payload pk case gucHeaders of
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected Left _ -> return . errorResponseFor $ GucHeadersError
-- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done Right ghdrs -> do
if queryTotal /= 1 let headers = addHeadersIfNotIncluded (catMaybes [Just $ toHeader contentType, profileH]) (unwrapGucHeader <$> ghdrs)
then do (status, rBody) = if iPreferRepresentation apiRequest == Full then (status200, toS body) else (status204, mempty)
HT.condemn -- Makes sure the querystring pk matches the payload pk
return . errorResponseFor $ PutMatchingPkError -- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected
else -- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done
return $ if iPreferRepresentation apiRequest == Full if queryTotal /= 1
then responseLBS status200 [toHeader contentType] (toS body) then do
else responseLBS status204 [] "" HT.condemn
return . errorResponseFor $ PutMatchingPkError
else
return $ responseLBS status headers rBody
(ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) -> (ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
case mutateSqlParts tSchema tName of case mutateSqlParts tSchema tName of
@@ -235,65 +252,68 @@ app dbStructure proc cols conf apiRequest =
let stm = createWriteStatement sq mq let stm = createWriteStatement sq mq
(contentType == CTSingularJSON) False (contentType == CTSingularJSON) False
(contentType == CTTextCSV) (contentType == CTTextCSV)
(iPreferRepresentation apiRequest) [] (iPreferRepresentation apiRequest) [] pgVer
row <- H.statement mempty stm row <- H.statement mempty stm
let (_, queryTotal, _, body) = extractQueryResult row let (_, queryTotal, _, body, gucHeaders) = row
r = contentRangeH 1 0 $ case gucHeaders of
if shouldCount then Just queryTotal else Nothing Left _ -> return . errorResponseFor $ GucHeadersError
if contentType == CTSingularJSON Right ghdrs -> do
&& queryTotal /= 1 let
&& iPreferRepresentation apiRequest == Full status = if iPreferRepresentation apiRequest == Full then status200 else status204
then do contentRangeHeader = contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing
HT.condemn (ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
return . errorResponseFor . singularityError $ queryTotal then ([Just $ toHeader contentType, profileH], toS body)
else else ([], mempty)
return $ if iPreferRepresentation apiRequest == Full headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs)
then responseLBS status200 [toHeader contentType, r] (toS body) if contentType == CTSingularJSON
else responseLBS status204 [r] "" && queryTotal /= 1
then do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
else
return $ responseLBS status headers rBody
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) -> (ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) ->
let mTable = find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure) in let mTable = find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure) in
case mTable of case mTable of
Nothing -> return notFound Nothing -> return notFound
Just table -> Just table ->
let acceptH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") in let allowH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET")
return $ responseLBS status200 [allOrigins, acceptH] "" allOrigins = ("Access-Control-Allow-Origin", "*") :: Header in
return $ responseLBS status200 [allOrigins, allowH] mempty
(ActionInvoke _, TargetProc qi _, Just pJson) -> (ActionInvoke invMethod, TargetProc qi@(QualifiedIdentifier tSchema pName) _, Just pJson) ->
let returnsScalar = case proc of let tName = fromMaybe pName $ procTableName =<< proc in
Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True case readSqlParts tSchema tName of
_ -> False
rpcBinaryField = if returnsScalar
then Right Nothing
else binaryField contentType rawContentTypes =<< fldNames
parts = (,) <$> readSqlParts <*> rpcBinaryField in
case parts of
Left errorResponse -> return errorResponse Left errorResponse -> return errorResponse
Right ((q, cq), bField) -> do Right (q, cq, bField, returning) -> do
let singular = contentType == CTSingularJSON let
row <- H.statement (toS $ pjRaw pJson) $ preferParams = iPreferParameters apiRequest
callProc qi (specifiedProcArgs cols proc) returnsScalar q cq shouldCount pq = requestToCallProcQuery qi (specifiedProcArgs cols proc) returnsScalar preferParams returning
singular (iPreferSingleObjectParameter apiRequest) stm = callProcStatement returnsScalar pq q cq shouldCount (contentType == CTSingularJSON)
(contentType == CTTextCSV) (contentType == CTTextCSV) (contentType `elem` rawContentTypes) (preferParams == Just MultipleObjects)
(contentType `elem` rawContentTypes) bField bField pgVer
(pgVersion dbStructure) row <- H.statement (toS $ pjRaw pJson) stm
let (tableTotal, queryTotal, body, jsonHeaders) = let (tableTotal, queryTotal, body, gucHeaders) = row
fromMaybe (Just 0, 0, "[]", "[]") row case gucHeaders of
(status, contentRange) = rangeHeader queryTotal tableTotal
decodedHeaders = first toS $ JSON.eitherDecode $ toS jsonHeaders :: Either Text [GucHeader]
case decodedHeaders of
Left _ -> return . errorResponseFor $ GucHeadersError Left _ -> return . errorResponseFor $ GucHeadersError
Right hs -> Right ghdrs -> do
if singular && queryTotal /= 1 let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal tableTotal
headers = addHeadersIfNotIncluded
(catMaybes [Just $ toHeader contentType, Just contentRange, profileH])
(unwrapGucHeader <$> ghdrs)
rBody = if invMethod == InvHead then mempty else toS body
if contentType == CTSingularJSON && queryTotal /= 1
then do then do
HT.condemn HT.condemn
return . errorResponseFor . singularityError $ queryTotal return . errorResponseFor . singularityError $ queryTotal
else return $ responseLBS status ([toHeader contentType, contentRange] ++ toHeaders hs) (toS body) else
return $ responseLBS status headers rBody
(ActionInspect, TargetDefaultSpec, Nothing) -> do (ActionInspect headersOnly, TargetDefaultSpec tSchema, Nothing) -> do
let host = configHost conf let host = configHost conf
port = toInteger $ configPort conf port = toInteger $ configPort conf
proxy = pickProxy $ toS <$> configProxyUri conf proxy = pickProxy $ toS <$> configOpenAPIProxyUri conf
uri Nothing = ("http", host, port, "/") uri Nothing = ("http", host, port, "/")
uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b) uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b)
uri' = uri proxy uri' = uri proxy
@@ -301,43 +321,52 @@ app dbStructure proc cols conf apiRequest =
toTableInfo = map (\t -> let (s, tn) = (tableSchema t, tableName t) in (t, tableCols dbStructure s tn, tablePKCols dbStructure s tn)) 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 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 body <- encodeApi <$>
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body H.statement tSchema accessibleTables <*>
H.statement tSchema schemaDescription <*>
H.statement tSchema accessibleProcs
return $ responseLBS status200 (catMaybes [Just $ toHeader CTOpenAPI, profileH]) (if headersOnly then mempty else toS body)
_ -> return notFound _ -> return notFound
where where
notFound = responseLBS status404 [] "" notFound = responseLBS status404 [] ""
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header maxRows = configMaxRows conf
shouldCount = iPreferCount apiRequest exactCount = iPreferCount apiRequest == Just ExactCount
schema = toS $ configSchema conf estimatedCount = iPreferCount apiRequest == Just EstimatedCount
topLevelRange = fromMaybe allRange $ M.lookup "limit" $ iRange apiRequest plannedCount = iPreferCount apiRequest == Just PlannedCount
rangeHeader queryTotal tableTotal = shouldCount = exactCount || estimatedCount
let lower = rangeOffset topLevelRange topLevelRange = iTopLevelRange apiRequest
upper = lower + toInteger queryTotal - 1 returnsScalar = maybe False procReturnsScalar proc
contentRange = contentRangeH lower upper (toInteger <$> tableTotal) pgVer = pgVersion dbStructure
status = rangeStatus lower upper (toInteger <$> tableTotal) profileH = contentProfileH <$> iProfile apiRequest
in (status, contentRange)
readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) proc apiRequest readSqlParts s t =
fldNames = fieldNames <$> readReq let
readDbRequest = DbRead <$> readReq readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest
selectQuery = requestToQuery schema False <$> readDbRequest returnings :: ReadRequest -> Either Response [FieldName]
countQuery = requestToCountQuery schema <$> readDbRequest returnings rr = Right (returningCols rr)
readSqlParts = (,) <$> selectQuery <*> countQuery in
mutationDbRequest s t = mutateRequest apiRequest t cols (tablePKCols dbStructure s t) =<< fldNames (,,,) <$>
mutateSqlParts s t = (readRequestToQuery <$> readReq) <*>
(,) <$> selectQuery (readRequestToCountQuery <$> readReq) <*>
<*> (requestToQuery schema False . DbMutate <$> mutationDbRequest s t) (binaryField contentType rawContentTypes returnsScalar =<< readReq) <*>
rawContentTypes = (returnings =<< readReq)
(decodeContentType <$> configRawMediaTypes conf) `L.union`
[ CTOctetStream, CTTextPlain ] mutateSqlParts s t =
let
readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest
mutReq = mutateRequest s t apiRequest cols (tablePKCols dbStructure s t) =<< readReq
in
(,) <$>
(readRequestToQuery <$> readReq) <*>
(mutateRequestToQuery <$> mutReq)
responseContentTypeOrError :: [ContentType] -> [ContentType] -> Action -> Target -> Either Response ContentType responseContentTypeOrError :: [ContentType] -> [ContentType] -> Action -> Target -> Either Response ContentType
responseContentTypeOrError accepts rawContentTypes action target = serves contentTypesForRequest accepts responseContentTypeOrError accepts rawContentTypes action target = serves contentTypesForRequest accepts
where where
contentTypesForRequest = case action of contentTypesForRequest = case action of
ActionRead -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ActionRead _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
++ rawContentTypes ++ rawContentTypes
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
@@ -345,7 +374,7 @@ responseContentTypeOrError accepts rawContentTypes action target = serves conten
ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
++ rawContentTypes ++ rawContentTypes
++ [CTOpenAPI | tpIsRootSpec target] ++ [CTOpenAPI | tpIsRootSpec target]
ActionInspect -> [CTOpenAPI, CTApplicationJSON] ActionInspect _ -> [CTOpenAPI, CTApplicationJSON]
ActionInfo -> [CTTextCSV] ActionInfo -> [CTTextCSV]
ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
serves sProduces cAccepts = serves sProduces cAccepts =
@@ -357,41 +386,34 @@ responseContentTypeOrError accepts rawContentTypes action target = serves conten
| If raw(binary) output is requested, check that ContentType is one of the admitted rawContentTypes and that | If raw(binary) output is requested, check that ContentType is one of the admitted rawContentTypes and that
| `?select=...` contains only one field other than `*` | `?select=...` contains only one field other than `*`
-} -}
binaryField :: ContentType -> [ContentType]-> [FieldName] -> Either Response (Maybe FieldName) binaryField :: ContentType -> [ContentType] -> Bool -> ReadRequest -> Either Response (Maybe FieldName)
binaryField ct rawContentTypes fldNames binaryField ct rawContentTypes isScalarProc readReq
| isScalarProc = Right Nothing
| ct `elem` rawContentTypes = | ct `elem` rawContentTypes =
let fieldName = headMay fldNames in let fieldName = headMay fldNames in
if length fldNames == 1 && fieldName /= Just "*" if length fldNames == 1 && fieldName /= Just "*"
then Right fieldName then Right fieldName
else Left . errorResponseFor $ BinaryFieldError ct else Left . errorResponseFor $ BinaryFieldError ct
| otherwise = Right Nothing | otherwise = Right Nothing
where
fldNames = fstFieldNames readReq
splitKeyValue :: BS.ByteString -> (BS.ByteString, BS.ByteString) locationH :: TableName -> [BS.ByteString] -> Header
splitKeyValue kv = (k, BS.tail v) locationH tName fields =
where (k, v) = BS.break (== '=') kv let
locationFields = renderSimpleQuery True $ splitKeyValue <$> fields
in
(hLocation, "/" <> toS tName <> locationFields)
where
splitKeyValue :: BS.ByteString -> (BS.ByteString, BS.ByteString)
splitKeyValue kv =
let (k, v) = BS.break (== '=') kv
in (k, BS.tail v)
renderLocationFields :: [BS.ByteString] -> BS.ByteString contentLocationH :: TableName -> ByteString -> Header
renderLocationFields fields = contentLocationH tName qString =
renderSimpleQuery True $ map splitKeyValue fields ("Content-Location", "/" <> toS tName <> if BS.null qString then mempty else "?" <> toS qString)
rangeStatus :: Integer -> Integer -> Maybe Integer -> Status contentProfileH :: Schema -> Header
rangeStatus _ _ Nothing = status200 contentProfileH schema =
rangeStatus lower upper (Just total) ("Content-Profile", toS schema)
| lower > total = status416
| (1 + upper - lower) < total = status206
| otherwise = status200
contentRangeH :: (Integral a, Show a) => a -> a -> Maybe a -> 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
extractQueryResult :: Maybe ResultsWithCount -> ResultsWithCount
extractQueryResult = fromMaybe (Nothing, 0, [], "")
+2 -1
View File
@@ -31,7 +31,8 @@ import Control.Lens.Operators
import Crypto.JWT import Crypto.JWT
import PostgREST.Types import PostgREST.Types
import Protolude import Protolude hiding (toS)
import Protolude.Conv (toS)
{-| {-|
Possible situations encountered with client JWTs Possible situations encountered with client JWTs
+40 -19
View File
@@ -32,22 +32,23 @@ import qualified Data.CaseInsensitive as CI
import qualified Data.Configurator as C import qualified Data.Configurator as C
import qualified Text.PrettyPrint.ANSI.Leijen as L import qualified Text.PrettyPrint.ANSI.Leijen as L
import Control.Exception (Handler (..))
import Control.Lens (preview) import Control.Lens (preview)
import Control.Monad (fail) import Control.Monad (fail)
import Crypto.JWT (StringOrURI, stringOrUri) import Crypto.JWT (StringOrURI, stringOrUri)
import Data.List (lookup) import Data.List (lookup)
import Data.List.NonEmpty (fromList)
import Data.Scientific (floatingOrInteger) import Data.Scientific (floatingOrInteger)
import Data.Text (dropEnd, dropWhileEnd, import Data.Text (dropEnd, dropWhileEnd,
intercalate, lines, splitOn, intercalate, splitOn, strip, take,
strip, take, unpack) unpack)
import Data.Text.Encoding (encodeUtf8)
import Data.Text.IO (hPutStrLn) import Data.Text.IO (hPutStrLn)
import Data.Version (versionBranch) import Data.Version (versionBranch)
import Development.GitRev (gitHash) import Development.GitRev (gitHash)
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..)) import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
import Numeric (readOct)
import Paths_postgrest (version) import Paths_postgrest (version)
import System.IO.Error (IOError) import System.IO.Error (IOError)
import System.Posix.Types (FileMode)
import Control.Applicative import Control.Applicative
import Data.Monoid import Data.Monoid
@@ -58,21 +59,22 @@ import Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>))
import PostgREST.Error (ApiRequestError (..)) import PostgREST.Error (ApiRequestError (..))
import PostgREST.Parsers (pRoleClaimKey) import PostgREST.Parsers (pRoleClaimKey)
import PostgREST.Types (JSPath, JSPathExp (..), import PostgREST.Types (JSPath, JSPathExp (..))
QualifiedIdentifier (..))
import Protolude hiding (concat, hPutStrLn, intercalate, null, import Protolude hiding (concat, hPutStrLn, intercalate, null,
take, (<>)) take, toS, (<>))
import Protolude.Conv (toS)
-- | Config file settings for the server -- | Config file settings for the server
data AppConfig = AppConfig { data AppConfig = AppConfig {
configDatabase :: Text configDatabase :: Text
, configAnonRole :: Text , configAnonRole :: Text
, configProxyUri :: Maybe Text , configOpenAPIProxyUri :: Maybe Text
, configSchema :: Text , configSchemas :: NonEmpty Text
, configHost :: Text , configHost :: Text
, configPort :: Int , configPort :: Int
, configSocket :: Maybe Text , configSocket :: Maybe FilePath
, configSocketMode :: Either Text FileMode
, configJwtSecret :: Maybe B.ByteString , configJwtSecret :: Maybe B.ByteString
, configJwtSecretIsBase64 :: Bool , configJwtSecretIsBase64 :: Bool
@@ -87,7 +89,7 @@ data AppConfig = AppConfig {
, configRoleClaimKey :: Either ApiRequestError JSPath , configRoleClaimKey :: Either ApiRequestError JSPath
, configExtraSearchPath :: [Text] , configExtraSearchPath :: [Text]
, configRootSpec :: Maybe QualifiedIdentifier , configRootSpec :: Maybe Text
, configRawMediaTypes :: [B.ByteString] , configRawMediaTypes :: [B.ByteString]
} }
@@ -137,7 +139,7 @@ readOptions = do
-- Now read the actual config file -- Now read the actual config file
conf <- catches (C.load cfgPath) conf <- catches (C.load cfgPath)
[ Handler (\(ex :: IOError) -> exitErr $ "Cannot open config file:\n\t" <> show ex) [ Handler (\(ex :: IOError) -> exitErr $ "Cannot open config file:\n\t" <> show ex)
, Handler (\(C.ParseError err) -> exitErr $ "Error parsing config file:\n\t" <> err) , Handler (\(C.ParseError err) -> exitErr $ "Error parsing config file:\n" <> err)
] ]
case C.runParser parseConfig conf of case C.runParser parseConfig conf of
@@ -147,16 +149,16 @@ readOptions = do
return appConf return appConf
where where
dbSchema = reqString "db-schema"
parseConfig = parseConfig =
AppConfig AppConfig
<$> reqString "db-uri" <$> reqString "db-uri"
<*> reqString "db-anon-role" <*> reqString "db-anon-role"
<*> optString "server-proxy-uri" <*> optString "openapi-server-proxy-uri"
<*> dbSchema <*> (fromList . splitOnCommas <$> reqValue "db-schema")
<*> (fromMaybe "!4" <$> optString "server-host") <*> (fromMaybe "!4" <$> optString "server-host")
<*> (fromMaybe 3000 <$> optInt "server-port") <*> (fromMaybe 3000 <$> optInt "server-port")
<*> optString "server-unix-socket" <*> (fmap unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode"
<*> (fmap encodeUtf8 <$> optString "jwt-secret") <*> (fmap encodeUtf8 <$> optString "jwt-secret")
<*> (fromMaybe False <$> optBool "secret-is-base64") <*> (fromMaybe False <$> optBool "secret-is-base64")
<*> parseJwtAudience "jwt-aud" <*> parseJwtAudience "jwt-aud"
@@ -168,9 +170,22 @@ readOptions = do
<*> (fmap (fmap coerceText) <$> C.subassocs "app.settings" C.value) <*> (fmap (fmap coerceText) <$> C.subassocs "app.settings" C.value)
<*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> optValue "role-claim-key") <*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> optValue "role-claim-key")
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path") <*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
<*> ((\x y -> QualifiedIdentifier x <$> y) <$> dbSchema <*> optString "root-spec") <*> optString "root-spec"
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types") <*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
parseSocketFileMode :: C.Key -> C.Parser C.Config (Either Text FileMode)
parseSocketFileMode k =
C.optional k C.string >>= \case
Nothing -> pure $ Right 432 -- return default 660 mode if no value was provided
Just fileModeText ->
case (readOct . unpack) fileModeText of
[] ->
pure $ Left "Invalid server-unix-socket-mode: not an octal"
(fileMode, _):_ ->
if fileMode < 384 || fileMode > 511
then pure $ Left "Invalid server-unix-socket-mode: needs to be between 600 and 777"
else pure $ Right fileMode
parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI) parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI)
parseJwtAudience k = parseJwtAudience k =
C.optional k C.string >>= \case C.optional k C.string >>= \case
@@ -183,6 +198,9 @@ readOptions = do
reqString :: C.Key -> C.Parser C.Config Text reqString :: C.Key -> C.Parser C.Config Text
reqString k = C.required k C.string reqString k = C.required k C.string
reqValue :: C.Key -> C.Parser C.Config C.Value
reqValue k = C.required k C.value
optString :: C.Key -> C.Parser C.Config (Maybe Text) optString :: C.Key -> C.Parser C.Config (Maybe Text)
optString k = mfilter (/= "") <$> C.optional k C.string optString k = mfilter (/= "") <$> C.optional k C.string
@@ -250,13 +268,16 @@ readOptions = do
|## unix socket location |## unix socket location
|## if specified it takes precedence over server-port |## if specified it takes precedence over server-port
|# server-unix-socket = "/tmp/pgrst.sock" |# server-unix-socket = "/tmp/pgrst.sock"
|## unix socket file mode
|## when none is provided, 660 is applied by default
|# server-unix-socket-mode = "660"
| |
|## base url for swagger output |## base url for swagger output
|# server-proxy-uri = "" |# openapi-server-proxy-uri = ""
| |
|## choose a secret, JSON Web Key (or set) to enable JWT auth |## choose a secret, JSON Web Key (or set) to enable JWT auth
|## (use "@filename" to load from separate file) |## (use "@filename" to load from separate file)
|# jwt-secret = "foo" |# jwt-secret = "secret_with_at_least_32_characters"
|# secret-is-base64 = false |# secret-is-base64 = false
|# jwt-aud = "your_audience_claim" |# jwt-aud = "your_audience_claim"
| |
+187 -196
View File
@@ -14,248 +14,218 @@ A query tree is built in case of resource embedding. By inferring the relationsh
module PostgREST.DbRequestBuilder ( module PostgREST.DbRequestBuilder (
readRequest readRequest
, mutateRequest , mutateRequest
, fieldNames , returningCols
) where ) where
import qualified Data.ByteString.Char8 as BS import qualified Data.HashMap.Strict as M
import qualified Data.HashMap.Strict as M import qualified Data.Set as S
import qualified Data.Set as S
import Control.Arrow ((***)) import Control.Arrow ((***))
import Control.Lens.Getter (view)
import Control.Lens.Tuple (_1)
import Data.Either.Combinators (mapLeft) import Data.Either.Combinators (mapLeft)
import Data.Foldable (foldr1) import Data.Foldable (foldr1)
import Data.List (delete) import Data.List (delete)
import Data.Maybe (fromJust)
import Data.Text (isInfixOf) import Data.Text (isInfixOf)
import Text.Regex.TDFA ((=~))
import Unsafe (unsafeHead)
import Control.Applicative import Control.Applicative
import Data.Tree import Data.Tree
import Network.Wai import Network.Wai
import PostgREST.ApiRequest (Action (..), ApiRequest (..), import PostgREST.ApiRequest (Action (..), ApiRequest (..))
PreferRepresentation (..),
PreferRepresentation (..), Target (..))
import PostgREST.Error (ApiRequestError (..), errorResponseFor) import PostgREST.Error (ApiRequestError (..), errorResponseFor)
import PostgREST.Parsers import PostgREST.Parsers
import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange) import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange)
import PostgREST.Types import PostgREST.Types
import Protolude hiding (from) import Protolude hiding (from)
readRequest :: Maybe Integer -> [Relation] -> Maybe ProcDescription -> ApiRequest -> Either Response ReadRequest readRequest :: Schema -> TableName -> Maybe Integer -> [Relation] -> ApiRequest -> Either Response ReadRequest
readRequest maxRows allRels proc apiRequest = readRequest schema rootTableName maxRows allRels apiRequest =
mapLeft errorResponseFor $ mapLeft errorResponseFor $
treeRestrictRange maxRows =<< treeRestrictRange maxRows =<<
augumentRequestWithJoin schema relations =<< augmentRequestWithJoin schema rootRels =<<
addFiltersOrdersRanges apiRequest <*> addFiltersOrdersRanges apiRequest <*>
(buildReadRequest <$> pRequestSelect (iSelect apiRequest)) (initReadRequest rootName <$> pRequestSelect sel)
where where
action = iAction apiRequest sel = fromMaybe "*" $ iSelect apiRequest -- default to all columns requested (SELECT *) for a non existent ?select querystring param
(schema, rootTableName) = fromJust $ -- Make it safe (rootName, rootRels) = rootWithRels schema rootTableName allRels (iAction apiRequest)
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 -- Get the root table name with its relationships according to the Action type.
-- This is done because of the shape of the final SQL Query. The mutation cases are wrapped in a WITH {sourceCTEName}(see Statements.hs).
-- So we need a FROM {sourceCTEName} instead of FROM {tableName}.
rootWithRels :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation])
rootWithRels schema rootTableName allRels action = case action of
ActionRead _ -> (QualifiedIdentifier schema rootTableName, allRels) -- normal read case
_ -> (QualifiedIdentifier mempty sourceCTEName, mapMaybe toSourceRel allRels ++ allRels) -- mutation cases and calling proc
where
-- To enable embedding in the sourceCTEName cases we need to replace the foreign key tableName in the Relation
-- with {sourceCTEName}. This way findRel can find relationships with sourceCTEName.
toSourceRel :: Relation -> Maybe Relation
toSourceRel r@Relation{relTable=t}
| rootTableName == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
| otherwise = Nothing
-- Build tree with a Depth attribute so when a self join occurs we can differentiate the parent and child tables by having -- Build the initial 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. -- an alias like "table_depth", this is related to http://github.com/PostgREST/postgrest/issues/987.
buildReadRequest :: [Tree SelectItem] -> ReadRequest initReadRequest :: QualifiedIdentifier -> [Tree SelectItem] -> ReadRequest
buildReadRequest fieldTree = initReadRequest rootQi =
let rootDepth = 0 foldr (treeEntry rootDepth) initial
rootNodeName = if action == ActionRead then rootTableName else sourceCTEName in where
foldr (treeEntry rootDepth) (Node (Select [] rootNodeName Nothing [] [] [] [] allRange, (rootNodeName, Nothing, Nothing, Nothing, rootDepth)) []) fieldTree rootDepth = 0
where rootSchema = qiSchema rootQi
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest rootName = qiName rootQi
treeEntry depth (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) = initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, rootDepth)) []
let nxtDepth = succ depth in treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
case fldForest of treeEntry depth (Node fld@((fn, _),_,alias, embedHint) fldForest) (Node (q, i) rForest) =
[] -> Node (q {select=fld:select q}, i) rForest let nxtDepth = succ depth in
_ -> Node (q, i) $ case fldForest of
foldr (treeEntry nxtDepth) (Node (Select [] fn Nothing [] [] [] [] allRange, (fn, Nothing, alias, relationDetail, nxtDepth)) []) fldForest:rForest [] -> Node (q {select=fld:select q}, i) rForest
_ -> Node (q, i) $
relations :: [Relation] foldr (treeEntry nxtDepth)
relations = case action of (Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange,
ActionCreate -> fakeSourceRelations ++ allRels (fn, Nothing, alias, embedHint, nxtDepth)) [])
ActionUpdate -> fakeSourceRelations ++ allRels fldForest:rForest
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 :: Maybe Integer -> ReadRequest -> Either ApiRequestError ReadRequest
treeRestrictRange maxRows_ request = pure $ nodeRestrictRange maxRows_ `fmap` request treeRestrictRange maxRows request = pure $ nodeRestrictRange maxRows <$> request
where where
nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode
nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i) nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i)
augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest augmentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest
augumentRequestWithJoin schema allRels request = augmentRequestWithJoin schema allRels request =
addRelations schema allRels Nothing request addRels schema allRels Nothing request
>>= addJoinConditions schema Nothing >>= addJoinConditions Nothing
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest addRels :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) = addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, hint, depth)) forest) =
case parentNode of case parentNode of
Just (Node (Select{from=parentNodeTable}, _) _) -> Just (Node (Select{from=parentNodeQi}, _) _) ->
let newFrom r = if tbl == nodeName then tableName (relTable r) else tbl let newFrom r = if qiName tbl == nodeName then tableQi (relFTable r) else tbl
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel
rel :: Either ApiRequestError Relation rel = findRel schema allRels (qiName parentNodeQi) nodeName hint
rel = note (NoRelationBetween parentNodeTable nodeName) $ in
findRelation schema allRelations nodeName parentNodeTable relationDetail in
Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest) Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest)
_ -> _ ->
let rn = (query, (nodeName, Just r, alias, Nothing, depth)) let rn = (query, (nodeName, Nothing, alias, Nothing, depth)) in
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) Node rn <$> updateForest (Just $ Node rn forest)
where where
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest] updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
updateForest rq = mapM (addRelations schema allRelations rq) forest updateForest rq = mapM (addRels schema allRels rq) forest
findRelation :: Schema -> [Relation] -> NodeName -> TableName -> Maybe RelationDetail -> Maybe Relation -- Finds a relationship between an origin and a target in the request: /origin?select=target(*)
findRelation schema allRelations nodeTableName parentNodeTableName relationDetail = -- If more than one relationship is found then the request is ambiguous and we return an error.
find (\Relation{relTable, relColumns, relFTable, relFColumns, relType, relLinkTable} -> -- In that case the request can be disambiguated by adding precision to the target or by using a hint: /origin?select=target!hint(*)
-- Both relation ends need to be on the exposed schema -- The elements will be matched according to these rules:
schema == tableSchema relTable && schema == tableSchema relFTable && -- origin = table / view
case relationDetail of -- target = table / view / constraint / column-from-origin
Nothing -> -- hint = table / view / constraint / column-from-origin / column-from-target
-- (hint can take table / view values to aid in finding the junction in an m2m relationship)
-- (request) => projects { ..., clients{...} } findRel :: Schema -> [Relation] -> NodeName -> NodeName -> Maybe EmbedHint -> Either ApiRequestError Relation
-- will match findRel schema allRels origin target hint =
-- (relation type) => parent case rel of
-- (entity) => clients {id} [] -> Left $ NoRelBetween origin target
-- (foriegn entity) => projects {client_id} [r] -> Right r
rs ->
-- Return error if more than one relationship is found, unless we're in a self reference case.
--
-- Here we handle a self reference relationship to not cause a breaking change:
-- In a self reference we get two relationships with the same foreign key and relTable/relFtable but with different cardinalities(m2o/o2m)
-- We output the O2M rel, the M2O rel can be obtained by using the origin column as an embed hint.
let [rel0, rel1] = take 2 rs in
if length rs == 2 && relConstraint rel0 == relConstraint rel1 && relTable rel0 == relTable rel1 && relFTable rel0 == relFTable rel1
then note (NoRelBetween origin target) (find (\r -> relType r == O2M) rs)
else Left $ AmbiguousRelBetween origin target rs
where
matchFKSingleCol hint_ cols = length cols == 1 && hint_ == (colName <$> head cols)
rel = filter (
\Relation{relTable, relColumns, relConstraint, relFTable, relFColumns, relType, relJunction} ->
-- Both relationship ends need to be on the exposed schema
schema == tableSchema relTable && schema == tableSchema relFTable &&
( (
nodeTableName == tableName relTable && -- match relation table name -- /projects?select=clients(*)
parentNodeTableName == tableName relFTable -- match relation foreign table name origin == tableName relTable && -- projects
) || target == tableName relFTable || -- clients
-- (request) => projects { ..., client_id{...} } -- /projects?select=projects_client_id_fkey(*)
-- will match (
-- (relation type) => parent origin == tableName relTable && -- projects
-- (entity) => clients {id} Just target == relConstraint -- projects_client_id_fkey
-- (foriegn entity) => projects {client_id} ) ||
( -- /projects?select=client_id(*)
parentNodeTableName == tableName relFTable && (
length relFColumns == 1 && origin == tableName relTable && -- projects
-- match common foreign key names(table_name_id, table_name_fk) to table_name matchFKSingleCol (Just target) relColumns -- client_id
(toS ("^" <> colName (unsafeHead relFColumns) <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (toS nodeTableName :: BS.ByteString) )
) && (
isNothing hint || -- hint is optional
-- /projects?select=clients!projects_client_id_fkey(*)
hint == relConstraint || -- projects_client_id_fkey
-- /projects?select=clients!client_id(*) or /projects?select=clients!id(*)
matchFKSingleCol hint relColumns || -- client_id
matchFKSingleCol hint relFColumns || -- id
-- /users?select=tasks!users_tasks(*)
(
relType == M2M && -- many-to-many between users and tasks
hint == (tableName . junTable <$> relJunction) -- users_tasks
)
) )
) allRels
-- (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
-- previousAlias is only used for the case of self joins -- previousAlias is only used for the case of self joins
addJoinConditions :: Schema -> Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
addJoinConditions schema previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, relation, _, _, depth)) forest) = addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, rel, _, _, depth)) forest) =
case relation of case rel of
Just Relation{relType=Root} -> Node node <$> updatedForest -- this is the root node Just r@Relation{relType=O2M} -> Node (augmentQuery r, nodeProps) <$> updatedForest
Just rel@Relation{relType=Parent} -> Node (augmentQuery rel, nodeProps) <$> updatedForest Just r@Relation{relType=M2O} -> Node (augmentQuery r, nodeProps) <$> updatedForest
Just rel@Relation{relType=Child} -> Node (augmentQuery rel, nodeProps) <$> updatedForest Just r@Relation{relType=M2M, relJunction=junction} ->
Just rel@Relation{relType=Many, relLinkTable=(Just linkTable)} -> case junction of
let rq = augmentQuery rel in Just Junction{junTable} ->
Node (rq{implicitJoins=tableName linkTable:implicitJoins rq}, nodeProps) <$> updatedForest let rq = augmentQuery r in
_ -> Left UnknownRelation Node (rq{implicitJoins=tableQi junTable:implicitJoins rq}, nodeProps) <$> updatedForest
Nothing ->
Left UnknownRelation
Nothing -> Node node <$> updatedForest
where where
newAlias = case isSelfJoin <$> relation of newAlias = case isSelfReference <$> rel of
Just True Just True
| depth /= 0 -> Just (tbl <> "_" <> show depth) -- root node doesn't get aliased | depth /= 0 -> Just (qiName tbl <> "_" <> show depth) -- root node doesn't get aliased
| otherwise -> Nothing | otherwise -> Nothing
_ -> Nothing _ -> Nothing
augmentQuery rel = augmentQuery r =
foldr foldr
(\jc rq@Select{joinConditions=jcs} -> rq{joinConditions=jc:jcs}) (\jc rq@Select{joinConditions=jcs} -> rq{joinConditions=jc:jcs})
query{fromAlias=newAlias} query{fromAlias=newAlias}
(getJoinConditions previousAlias newAlias rel) (getJoinConditions previousAlias newAlias r)
updatedForest = mapM (addJoinConditions schema newAlias) forest updatedForest = mapM (addJoinConditions newAlias) forest
-- previousAlias and newAlias are used in the case of self joins -- previousAlias and newAlias are used in the case of self joins
getJoinConditions :: Maybe Alias -> Maybe Alias -> Relation -> [JoinCondition] getJoinConditions :: Maybe Alias -> Maybe Alias -> Relation -> [JoinCondition]
getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) = getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, tableName=tN} cols _ Table{tableName=ftN} fCols typ jun) =
case typ of case typ of
Child -> O2M ->
zipWith (toJoinCondition tN ftN) cols fCols zipWith (toJoinCondition tN ftN) cols fCols
Parent -> M2O ->
zipWith (toJoinCondition tN ftN) cols fCols zipWith (toJoinCondition tN ftN) cols fCols
Many -> M2M -> case jun of
let ltN = maybe "" tableName lt in Just (Junction jt _ jc1 _ jc2) ->
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2) let jtn = tableName jt in
Root -> witness zipWith (toJoinCondition tN jtn) cols jc1 ++ zipWith (toJoinCondition ftN jtn) fCols jc2
Nothing -> []
where where
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
toJoinCondition tb ftb c fc = toJoinCondition tb ftb c fc =
let qi1 = QualifiedIdentifier tSchema tb let qi1 = removeSourceCTESchema tSchema tb
qi2 = QualifiedIdentifier tSchema ftb in qi2 = removeSourceCTESchema tSchema ftb in
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) newAlias, colName c) JoinCondition (maybe qi1 (QualifiedIdentifier mempty) previousAlias, colName c)
(maybe qi2 (QualifiedIdentifier mempty) previousAlias, colName fc) (maybe qi2 (QualifiedIdentifier mempty) newAlias, colName fc)
-- On mutation and calling proc cases we wrap the target table in a WITH {sourceCTEName}
-- if this happens remove the schema `FROM "schema"."{sourceCTEName}"` and use only the
-- `FROM "{sourceCTEName}"`. If the schema remains the FROM would be invalid.
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then mempty else schema) tbl
addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> ReadRequest) addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> ReadRequest)
addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [ addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
@@ -266,7 +236,7 @@ addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
] ]
{- {-
The esence of what is going on above is that we are composing tree functions 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 of type (ReadRequest->ReadRequest) that are in (Either ApiRequestError a) context
-} -}
where where
filters :: Either ApiRequestError [(EmbedPath, Filter)] filters :: Either ApiRequestError [(EmbedPath, Filter)]
@@ -278,7 +248,7 @@ addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
(flts, logFrst) = (flts, logFrst) =
case action of case action of
ActionInvoke _ -> (iFilters apiRequest, iLogic apiRequest) ActionInvoke _ -> (iFilters apiRequest, iLogic apiRequest)
ActionRead -> (iFilters apiRequest, iLogic apiRequest) ActionRead _ -> (iFilters apiRequest, iLogic apiRequest)
_ -> join (***) (filter (( "." `isInfixOf` ) . fst)) (iFilters apiRequest, iLogic apiRequest) _ -> join (***) (filter (( "." `isInfixOf` ) . fst)) (iFilters apiRequest, iLogic apiRequest)
orders :: Either ApiRequestError [(EmbedPath, [OrderTerm])] orders :: Either ApiRequestError [(EmbedPath, [OrderTerm])]
orders = mapM pRequestOrder $ iOrder apiRequest orders = mapM pRequestOrder $ iOrder apiRequest
@@ -318,11 +288,15 @@ addProperty f (targetNodeName:remainingPath, a) (Node rn forest) =
where where
pathNode = find (\(Node (_,(nodeName,_,alias,_,_)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest pathNode = find (\(Node (_,(nodeName,_,alias,_,_)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest
mutateRequest :: ApiRequest -> TableName -> S.Set FieldName -> [FieldName] -> [FieldName] -> Either Response MutateRequest mutateRequest :: Schema -> TableName -> ApiRequest -> S.Set FieldName -> [FieldName] -> ReadRequest -> Either Response MutateRequest
mutateRequest apiRequest tName cols pkCols fldNames = mapLeft errorResponseFor $ mutateRequest schema tName apiRequest cols pkCols readReq = mapLeft errorResponseFor $
case action of case action of
ActionCreate -> Right $ Insert tName cols ((,) <$> iPreferResolution apiRequest <*> Just pkCols) [] returnings ActionCreate -> do
ActionUpdate -> Update tName cols <$> combinedLogic <*> pure returnings confCols <- case iOnConflict apiRequest of
Nothing -> pure pkCols
Just param -> pRequestOnConflict param
pure $ Insert qi cols ((,) <$> iPreferResolution apiRequest <*> Just confCols) [] returnings
ActionUpdate -> Update qi cols <$> combinedLogic <*> pure returnings
ActionSingleUpsert -> ActionSingleUpsert ->
(\flts -> (\flts ->
if null (iLogic apiRequest) && if null (iLogic apiRequest) &&
@@ -331,14 +305,18 @@ mutateRequest apiRequest tName cols pkCols fldNames = mapLeft errorResponseFor $
all (\case all (\case
Filter _ (OpExpr False (Op "eq" _)) -> True Filter _ (OpExpr False (Op "eq" _)) -> True
_ -> False) flts _ -> False) flts
then Insert tName cols (Just (MergeDuplicates, pkCols)) <$> combinedLogic <*> pure returnings then Insert qi cols (Just (MergeDuplicates, pkCols)) <$> combinedLogic <*> pure returnings
else else
Left InvalidFilters) =<< filters Left InvalidFilters) =<< filters
ActionDelete -> Delete tName <$> combinedLogic <*> pure returnings ActionDelete -> Delete qi <$> combinedLogic <*> pure returnings
_ -> Left UnsupportedVerb _ -> Left UnsupportedVerb
where where
qi = QualifiedIdentifier schema tName
action = iAction apiRequest action = iAction apiRequest
returnings = if iPreferRepresentation apiRequest == None then [] else fldNames returnings =
if iPreferRepresentation apiRequest == None
then []
else returningCols readReq
filters = map snd <$> mapM pRequestFilter mutateFilters filters = map snd <$> mapM pRequestFilter mutateFilters
logic = map snd <$> mapM pRequestLogicTree logicFilters logic = map snd <$> mapM pRequestLogicTree logicFilters
combinedLogic = foldr addFilterToLogicForest <$> logic <*> filters combinedLogic = foldr addFilterToLogicForest <$> logic <*> filters
@@ -346,13 +324,26 @@ mutateRequest apiRequest tName cols pkCols fldNames = mapLeft errorResponseFor $
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest) (mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst) onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
fieldNames :: ReadRequest -> [FieldName] returningCols :: ReadRequest -> [FieldName]
fieldNames (Node (sel, _) forest) = returningCols rr@(Node _ forest) = returnings
map (fst . view _1) (select sel) ++ map colName fks
where where
fks = concatMap (fromMaybe [] . f) forest fldNames = fstFieldNames rr
f (Node (_, (_, Just Relation{relFColumns=cols, relType=Parent}, _, _, _)) _) = Just cols -- Without fkCols, when a mutateRequest to /projects?select=name,clients(name) occurs, the RETURNING SQL part would be
f _ = Nothing -- `RETURNING name`(see QueryBuilder).
-- This would make the embedding fail because the following JOIN would need the "client_id" column from projects.
-- So this adds the foreign key columns to ensure the embedding succeeds, result would be `RETURNING name, client_id`.
-- This also works for the other relType's.
fkCols = concat $ mapMaybe (\case
Node (_, (_, Just Relation{relColumns=cols, relType=relTyp}, _, _, _)) _ -> case relTyp of
O2M -> Just cols
M2O -> Just cols
M2M -> Just cols
_ -> Nothing
) forest
-- However if the "client_id" is present, e.g. mutateRequest to /projects?select=client_id,name,clients(name)
-- we would get `RETURNING client_id, name, client_id` and then we would produce the "column reference \"client_id\" is ambiguous"
-- error from PostgreSQL. So we deduplicate with Set:
returnings = S.toList . S.fromList $ fldNames ++ (colName <$> fkCols)
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree -- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
-- they are later concatenated with AND in the QueryBuilder -- they are later concatenated with AND in the QueryBuilder
+234 -297
View File
@@ -35,39 +35,27 @@ import Data.Set as S (fromList)
import Data.Text (breakOn, dropAround, split, import Data.Text (breakOn, dropAround, split,
splitOn, strip) splitOn, strip)
import GHC.Exts (groupWith) import GHC.Exts (groupWith)
import Protolude hiding (toS)
import Protolude.Conv (toS)
import Protolude.Unsafe (unsafeHead)
import Text.InterpolatedString.Perl6 (q, qc) import Text.InterpolatedString.Perl6 (q, qc)
import Unsafe (unsafeHead)
import Control.Applicative
import PostgREST.Private.Common
import PostgREST.Types import PostgREST.Types
import Protolude
column :: HD.Value a -> HD.Row a getDbStructure :: [Schema] -> PgVersion -> HT.Transaction DbStructure
column = HD.column . HD.nonNullable getDbStructure schemas pgVer = do
HT.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object
tabs <- HT.statement () allTables
cols <- HT.statement schemas $ allColumns tabs
srcCols <- HT.statement schemas $ allSourceColumns cols pgVer
m2oRels <- HT.statement () $ allM2ORels tabs cols
keys <- HT.statement () $ allPrimaryKeys tabs
procs <- HT.statement schemas allProcs
nullableColumn :: HD.Value a -> HD.Row (Maybe a) let rels = addM2MRels . addO2MRels $ addViewM2ORels srcCols m2oRels
nullableColumn = HD.column . HD.nullable
element :: HD.Value a -> HD.Array a
element = HD.element . HD.nonNullable
param :: HE.Value a -> HE.Params a
param = HE.param . HE.nonNullable
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 cols' = addForeignKeys rels cols
keys' = addViewPrimaryKeys syns keys keys' = addViewPrimaryKeys srcCols keys
return DbStructure { return DbStructure {
dbTables = tabs dbTables = tabs
@@ -102,13 +90,14 @@ decodeColumns tables =
<*> nullableColumn HD.text <*> nullableColumn HD.text
<*> nullableColumn HD.text <*> nullableColumn HD.text
decodeRelations :: [Table] -> [Column] -> HD.Result [Relation] decodeRels :: [Table] -> [Column] -> HD.Result [Relation]
decodeRelations tables cols = decodeRels tables cols =
mapMaybe (relationFromRow tables cols) <$> HD.rowList relRow mapMaybe (relFromRow tables cols) <$> HD.rowList relRow
where where
relRow = (,,,,,) relRow = (,,,,,,)
<$> column HD.text <$> column HD.text
<*> column HD.text <*> column HD.text
<*> column HD.text
<*> column (HD.array (HD.dimension replicateM (element HD.text))) <*> column (HD.array (HD.dimension replicateM (element HD.text)))
<*> column HD.text <*> column HD.text
<*> column HD.text <*> column HD.text
@@ -120,22 +109,30 @@ decodePks tables =
where where
pkRow = (,,) <$> column HD.text <*> column HD.text <*> column HD.text pkRow = (,,) <$> column HD.text <*> column HD.text <*> column HD.text
decodeSynonyms :: [Column] -> HD.Result [Synonym] decodeSourceColumns :: [Column] -> HD.Result [SourceColumn]
decodeSynonyms cols = decodeSourceColumns cols =
mapMaybe (synonymFromRow cols) <$> HD.rowList synRow mapMaybe (sourceColumnFromRow cols) <$> HD.rowList srcColRow
where where
synRow = (,,,,,) srcColRow = (,,,,,)
<$> column HD.text <*> column HD.text <$> column HD.text <*> column HD.text
<*> column HD.text <*> column HD.text <*> column HD.text <*> column HD.text
<*> column HD.text <*> column HD.text <*> column HD.text <*> column HD.text
decodeProcs :: HD.Result (M.HashMap Text [ProcDescription]) sourceColumnFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe SourceColumn
sourceColumnFromRow 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
decodeProcs :: HD.Result ProcsMap
decodeProcs = decodeProcs =
-- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance -- 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 map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addKey) <$> HD.rowList procRow
where where
tblRow = ProcDescription procRow = ProcDescription
<$> column HD.text <$> column HD.text
<*> column HD.text
<*> nullableColumn HD.text <*> nullableColumn HD.text
<*> (parseArgs <$> column HD.text) <*> (parseArgs <$> column HD.text)
<*> (parseRetType <*> (parseRetType
@@ -145,8 +142,8 @@ decodeProcs =
<*> column HD.char) <*> column HD.char)
<*> (parseVolatility <$> column HD.char) <*> (parseVolatility <$> column HD.char)
addName :: ProcDescription -> (Text, ProcDescription) addKey :: ProcDescription -> (QualifiedIdentifier, ProcDescription)
addName pd = (pdName pd, pd) addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd)
parseArgs :: Text -> [PgArg] parseArgs :: Text -> [PgArg]
parseArgs = mapMaybe parseArg . filter (not . isPrefixOf "OUT" . toS) . map strip . split (==',') parseArgs = mapMaybe parseArg . filter (not . isPrefixOf "OUT" . toS) . map strip . split (==',')
@@ -179,31 +176,34 @@ decodeProcs =
| v == 's' = Stable | v == 's' = Stable
| otherwise = Volatile -- only 'v' can happen here | otherwise = Volatile -- only 'v' can happen here
allProcs :: H.Statement Schema (M.HashMap Text [ProcDescription]) allProcs :: H.Statement [Schema] ProcsMap
allProcs = H.Statement (toS procsSqlQuery) (param HE.text) decodeProcs True allProcs = H.Statement (toS sql) (arrayParam HE.text) decodeProcs True
where
sql = procsSqlQuery <> " WHERE pn.nspname = ANY($1)"
accessibleProcs :: H.Statement Schema (M.HashMap Text [ProcDescription]) accessibleProcs :: H.Statement Schema ProcsMap
accessibleProcs = H.Statement (toS sql) (param HE.text) decodeProcs True accessibleProcs = H.Statement (toS sql) (param HE.text) decodeProcs True
where where
sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')" sql = procsSqlQuery <> " WHERE pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')"
procsSqlQuery :: SqlQuery procsSqlQuery :: SqlQuery
procsSqlQuery = [q| procsSqlQuery = [q|
SELECT p.proname as "proc_name", SELECT
d.description as "proc_description", pn.nspname as proc_schema,
pg_get_function_arguments(p.oid) as "args", p.proname as proc_name,
tn.nspname as "rettype_schema", d.description as proc_description,
coalesce(comp.relname, t.typname) as "rettype_name", pg_get_function_arguments(p.oid) as args,
p.proretset as "rettype_is_setof", tn.nspname as rettype_schema,
t.typtype as "rettype_typ", coalesce(comp.relname, t.typname) as rettype_name,
p.provolatile p.proretset as rettype_is_setof,
t.typtype as rettype_typ,
p.provolatile
FROM pg_proc p FROM pg_proc p
JOIN pg_namespace pn ON pn.oid = p.pronamespace JOIN pg_namespace pn ON pn.oid = p.pronamespace
JOIN pg_type t ON t.oid = p.prorettype JOIN pg_type t ON t.oid = p.prorettype
JOIN pg_namespace tn ON tn.oid = t.typnamespace JOIN pg_namespace tn ON tn.oid = t.typnamespace
LEFT JOIN pg_class comp ON comp.oid = t.typrelid LEFT JOIN pg_class comp ON comp.oid = t.typrelid
LEFT JOIN pg_catalog.pg_description as d on d.objoid = p.oid 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 Schema (Maybe Text)
@@ -228,11 +228,22 @@ accessibleTables =
n.nspname as table_schema, n.nspname as table_schema,
relname as table_name, relname as table_name,
d.description as table_description, 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 ( c.relkind in ('r', 'v', 'f')
select 1 and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8
from pg_trigger -- The function `pg_relation_is_updateable` returns a bitmask where 8
where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69) -- corresponds to `1 << CMD_INSERT` in the PostgreSQL source code, i.e.
-- it's possible to insert into the relation.
or (exists (
select 1
from pg_trigger
where
pg_trigger.tgrelid = c.oid
and (pg_trigger.tgtype::integer & 69) = 69)
-- The trigger type `tgtype` is a bitmask where 69 corresponds to
-- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_INSERT
-- in the PostgreSQL source code.
)
) as insertable ) as insertable
from from
pg_class c pg_class c
@@ -242,9 +253,9 @@ accessibleTables =
c.relkind in ('v', 'r', 'm', 'f') c.relkind in ('v', 'r', 'm', 'f')
and n.nspname = $1 and n.nspname = $1
and ( and (
pg_has_role(c.relowner, 'USAGE'::text) pg_has_role(c.relowner, 'USAGE')
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text) or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')
) )
order by relname |] order by relname |]
@@ -254,20 +265,20 @@ addForeignKeys rels = map addFk
addFk col = col { colFK = fk col } addFk col = col { colFK = fk col }
fk col = find (lookupFn col) rels >>= relToFk col fk col = find (lookupFn col) rels >>= relToFk col
lookupFn :: Column -> Relation -> Bool lookupFn :: Column -> Relation -> Bool
lookupFn c Relation{relColumns=cs, relType=rty} = c `elem` cs && rty==Child lookupFn c Relation{relColumns=cs, relType=rty} = c `elem` cs && rty==M2O
relToFk col Relation{relColumns=cols, relFColumns=colsF} = do relToFk col Relation{relColumns=cols, relFColumns=colsF} = do
pos <- L.elemIndex col cols pos <- L.elemIndex col cols
colF <- atMay colsF pos colF <- atMay colsF pos
return $ ForeignKey colF return $ ForeignKey colF
{- {-
Adds Views Child Relations based on Synonyms found, the logic is as follows: Adds Views M2O Relations based on SourceColumns found, the logic is as follows:
Having a Relation{relTable=t1, relColumns=[c1], relFTable=t2, relFColumns=[c2], relType=Child} represented by: Having a Relation{relTable=t1, relColumns=[c1], relFTable=t2, relFColumns=[c2], relType=M2O} represented by:
t1.c1------t2.c2 t1.c1------t2.c2
When only having a t1_view.c1 synonym, we need to add a View to Table Child Relation When only having a t1_view.c1 source column, we need to add a View-Table M2O Relation
t1.c1----t2.c2 t1.c1----------t2.c2 t1.c1----t2.c2 t1.c1----------t2.c2
-> ________/ -> ________/
@@ -275,70 +286,72 @@ When only having a t1_view.c1 synonym, we need to add a View to Table Child Rela
t1_view.c1 t1_view.c1 t1_view.c1 t1_view.c1
When only having a t2_view.c2 synonym, we need to add a Table to View Child Relation When only having a t2_view.c2 source column, we need to add a Table-View M2O Relation
t1.c1----t2.c2 t1.c1----------t2.c2 t1.c1----t2.c2 t1.c1----------t2.c2
-> \________ -> \________
\ \
t2_view.c2 t2_view.c1 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 When having t1_view.c1 and a t2_view.c2 source columns, we need to add a View-View M2O Relation in addition to the prior
t1.c1----t2.c2 t1.c1----------t2.c2 t1.c1----t2.c2 t1.c1----------t2.c2
-> \________/ -> \________/
/ \ / \
t1_view.c1 t2_view.c2 t1_view.c1-------t2_view.c1 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. The logic for composite pks is similar just need to make sure all the Relation columns have source columns.
-} -}
addViewChildRelations :: [Synonym] -> [Relation] -> [Relation] addViewM2ORels :: [SourceColumn] -> [Relation] -> [Relation]
addViewChildRelations allSyns = concatMap (\rel -> addViewM2ORels allSrcCols = concatMap (\rel ->
rel : case rel of rel : case rel of
Relation{relType=Child, relTable, relColumns, relFTable, relFColumns} -> Relation{relType=M2O, relTable, relColumns, relConstraint, relFTable, relFColumns} ->
let colSynsGroupedByView :: [Column] -> [[Synonym]] let srcColsGroupedByView :: [Column] -> [[SourceColumn]]
colSynsGroupedByView relCols = L.groupBy (\(_, viewCol1) (_, viewCol2) -> colTable viewCol1 == colTable viewCol2) $ srcColsGroupedByView relCols = L.groupBy (\(_, viewCol1) (_, viewCol2) -> colTable viewCol1 == colTable viewCol2) $
filter (\(c, _) -> c `elem` relCols) allSyns filter (\(c, _) -> c `elem` relCols) allSrcCols
colsSyns = colSynsGroupedByView relColumns relSrcCols = srcColsGroupedByView relColumns
fColsSyns = colSynsGroupedByView relFColumns relFSrcCols = srcColsGroupedByView relFColumns
getView :: [Synonym] -> Table getView :: [SourceColumn] -> Table
getView = colTable . snd . unsafeHead getView = colTable . snd . unsafeHead
syns `allSynsOf` cols = S.fromList (fst <$> syns) == S.fromList cols srcCols `allSrcColsOf` cols = S.fromList (fst <$> srcCols) == S.fromList cols
-- Relation is dependent on the order of relColumns and relFColumns to get the join conditions right in the generated query. -- 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 -- So we need to change the order of the SourceColumns 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 -- TODO: 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 srcCols `sortAccordingTo` cols = sortOn (\(k, _) -> L.lookup k $ zip cols [0::Int ..]) srcCols
viewTableChild = viewTableM2O =
[ Relation (getView syns) (snd <$> syns `sortAccordingTo` relColumns) [ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
relFTable relFColumns relConstraint relFTable relFColumns
Child Nothing Nothing Nothing M2O Nothing
| syns <- colsSyns, syns `allSynsOf` relColumns ] | srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns ]
tableViewChild = tableViewM2O =
[ Relation relTable relColumns [ Relation relTable relColumns
(getView fSyns) (snd <$> fSyns `sortAccordingTo` relFColumns) relConstraint
Child Nothing Nothing Nothing (getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
| fSyns <- fColsSyns, fSyns `allSynsOf` relFColumns ] M2O Nothing
| fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
viewViewChild = viewViewM2O =
[ Relation (getView syns) (snd <$> syns `sortAccordingTo` relColumns) [ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
(getView fSyns) (snd <$> fSyns `sortAccordingTo` relFColumns) relConstraint
Child Nothing Nothing Nothing (getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
| syns <- colsSyns, syns `allSynsOf` relColumns M2O Nothing
, fSyns <- fColsSyns, fSyns `allSynsOf` relFColumns ] | srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns
, fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
in viewTableChild ++ tableViewChild ++ viewViewChild in viewTableM2O ++ tableViewM2O ++ viewViewM2O
_ -> []) _ -> [])
addParentRelations :: [Relation] -> [Relation] addO2MRels :: [Relation] -> [Relation]
addParentRelations = concatMap (\rel@(Relation t c ft fc _ _ _ _) -> [rel, Relation ft fc t c Parent Nothing Nothing Nothing]) addO2MRels = concatMap (\rel@(Relation t c cn ft fc _ _) -> [rel, Relation ft fc cn t c O2M Nothing])
addManyToManyRelations :: [Relation] -> [Relation] addM2MRels :: [Relation] -> [Relation]
addManyToManyRelations rels = rels ++ addMirrorRelation (mapMaybe link2Relation links) addM2MRels rels = rels ++ addMirrorRel (mapMaybe junction2Rel junctions)
where where
links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) rels junctions = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==M2O). relType) rels
groupFn :: Relation -> Text groupFn :: Relation -> Text
groupFn Relation{relTable=Table{tableSchema=s, tableName=t}} = s <> "_" <> t groupFn Relation{relTable=Table{tableSchema=s, tableName=t}} = s <> "_" <> t
-- Reference : https://wiki.haskell.org/99_questions/Solutions/26 -- Reference : https://wiki.haskell.org/99_questions/Solutions/26
@@ -346,19 +359,20 @@ addManyToManyRelations rels = rels ++ addMirrorRelation (mapMaybe link2Relation
combinations 0 _ = [ [] ] combinations 0 _ = [ [] ]
combinations n xs = [ y:ys | y:xs' <- tails xs combinations n xs = [ y:ys | y:xs' <- tails xs
, ys <- combinations (n-1) 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]) junction2Rel [
link2Relation [ Relation{relTable=jt, relColumns=jc1, relConstraint=const1, relFTable=t, relFColumns=c},
Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c}, Relation{ relColumns=jc2, relConstraint=const2, relFTable=ft, relFColumns=fc}
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) | jc1 /= jc2 && length jc1 == 1 && length jc2 == 1 = Just $ Relation t c Nothing ft fc M2M (Just $ Junction jt const1 jc1 const2 jc2)
| otherwise = Nothing | otherwise = Nothing
link2Relation _ = Nothing junction2Rel _ = Nothing
addMirrorRel = concatMap (\rel@(Relation t c _ ft fc _ (Just (Junction jt const1 jc1 const2 jc2))) ->
[rel, Relation ft fc Nothing t c M2M (Just (Junction jt const2 jc2 const1 jc1))])
addViewPrimaryKeys :: [Synonym] -> [PrimaryKey] -> [PrimaryKey] addViewPrimaryKeys :: [SourceColumn] -> [PrimaryKey] -> [PrimaryKey]
addViewPrimaryKeys syns = concatMap (\pk -> addViewPrimaryKeys srcCols = concatMap (\pk ->
let viewPks = (\(_, viewCol) -> PrimaryKey{pkTable=colTable viewCol, pkName=colName viewCol}) <$> let viewPks = (\(_, viewCol) -> PrimaryKey{pkTable=colTable viewCol, pkName=colName viewCol}) <$>
filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) syns in filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) srcCols in
pk : viewPks) pk : viewPks)
allTables :: H.Statement () [Table] allTables :: H.Statement () [Table]
@@ -370,13 +384,17 @@ allTables =
n.nspname AS table_schema, n.nspname AS table_schema,
c.relname AS table_name, c.relname AS table_name,
NULL AS table_description, NULL AS table_description,
c.relkind = 'r' OR (c.relkind IN ('v','f')) (
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8 c.relkind IN ('r', 'v','f')
OR (EXISTS AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
( SELECT 1 OR EXISTS (
SELECT 1
FROM pg_trigger FROM pg_trigger
WHERE pg_trigger.tgrelid = c.oid WHERE
AND (pg_trigger.tgtype::integer & 69) = 69) ) AS insertable pg_trigger.tgrelid = c.oid
AND (pg_trigger.tgtype::integer & 69) = 69
)
) AS insertable
FROM pg_class c FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('v','r','m','f') WHERE c.relkind IN ('v','r','m','f')
@@ -384,9 +402,9 @@ allTables =
GROUP BY table_schema, table_name, insertable GROUP BY table_schema, table_name, insertable
ORDER BY table_schema, table_name |] ORDER BY table_schema, table_name |]
allColumns :: [Table] -> H.Statement Schema [Column] allColumns :: [Table] -> H.Statement [Schema] [Column]
allColumns tabs = allColumns tabs =
H.Statement sql (param HE.text) (decodeColumns tabs) True H.Statement sql (arrayParam HE.text) (decodeColumns tabs) True
where where
sql = [q| sql = [q|
SELECT DISTINCT SELECT DISTINCT
@@ -403,9 +421,7 @@ allColumns tabs =
info.column_default AS default_value, info.column_default AS default_value,
array_to_string(enum_info.vals, ',') AS enum array_to_string(enum_info.vals, ',') AS enum
FROM ( FROM (
/* -- CTE based on pg_catalog to get PRIMARY/FOREIGN key and UNIQUE columns outside api schema
-- CTE based on pg_catalog to get only Primary and Foreign key columns outside api schema
*/
WITH key_columns AS ( WITH key_columns AS (
SELECT SELECT
r.oid AS r_oid, r.oid AS r_oid,
@@ -420,11 +436,11 @@ allColumns tabs =
pg_catalog.pg_class c, pg_catalog.pg_class c,
pg_catalog.pg_namespace n pg_catalog.pg_namespace n
WHERE WHERE
r.contype IN ('f', 'p') r.contype IN ('f', 'p', 'u')
AND c.relkind IN ('r', 'v', 'f', 'm') AND c.relkind IN ('r', 'v', 'f', 'm')
AND r.conrelid = c.oid AND r.conrelid = c.oid
AND c.relnamespace = n.oid AND c.relnamespace = n.oid
AND n.nspname NOT IN ('pg_catalog', 'information_schema', $1) AND n.nspname <> ANY (ARRAY['pg_catalog', 'information_schema'] || $1)
), ),
/* /*
-- CTE based on information_schema.columns -- CTE based on information_schema.columns
@@ -433,19 +449,16 @@ allColumns tabs =
-- limit columns to the ones in the api schema or PK/FK columns -- limit columns to the ones in the api schema or PK/FK columns
*/ */
columns AS ( columns AS (
SELECT current_database()::information_schema.sql_identifier AS table_catalog, SELECT
nc.nspname::information_schema.sql_identifier AS table_schema, nc.nspname::name AS table_schema,
c.relname::information_schema.sql_identifier AS table_name, c.relname::name AS table_name,
a.attname::information_schema.sql_identifier AS column_name, a.attname::name AS column_name,
d.description AS description, d.description AS description,
a.attnum::information_schema.cardinal_number AS ordinal_position, a.attnum::integer AS ordinal_position,
pg_get_expr(ad.adbin, ad.adrelid)::information_schema.character_data AS column_default, pg_get_expr(ad.adbin, ad.adrelid)::text AS column_default,
not (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS is_nullable,
CASE CASE
WHEN a.attnotnull OR t.typtype = 'd'::"char" AND t.typnotnull THEN 'NO'::text WHEN t.typtype = 'd' THEN
ELSE 'YES'::text
END::information_schema.yes_or_no AS is_nullable,
CASE
WHEN t.typtype = 'd'::"char" THEN
CASE CASE
WHEN bt.typelem <> 0::oid AND bt.typlen = (-1) THEN 'ARRAY'::text 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) WHEN nbt.nspname = 'pg_catalog'::name THEN format_type(t.typbasetype, NULL::integer)
@@ -457,77 +470,42 @@ allColumns tabs =
WHEN nt.nspname = 'pg_catalog'::name THEN format_type(a.atttypid, NULL::integer) WHEN nt.nspname = 'pg_catalog'::name THEN format_type(a.atttypid, NULL::integer)
ELSE format_type(a.atttypid, a.atttypmod) ELSE format_type(a.atttypid, a.atttypmod)
END END
END::information_schema.character_data AS data_type, END::text 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_max_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_truetypid(a.*, t.*),
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_truetypmod(a.*, t.*)
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, )::integer AS character_maximum_length,
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_numeric_precision(
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_truetypid(a.*, t.*),
information_schema._pg_interval_type(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.character_data AS interval_type, information_schema._pg_truetypmod(a.*, t.*)
NULL::integer::information_schema.cardinal_number AS interval_precision, )::integer AS numeric_precision,
NULL::character varying::information_schema.sql_identifier AS character_set_catalog, COALESCE(bt.typname, t.typname)::name AS udt_name,
NULL::character varying::information_schema.sql_identifier AS character_set_schema, (
NULL::character varying::information_schema.sql_identifier AS character_set_name, c.relkind in ('r', 'v', 'f')
CASE AND pg_column_is_updatable(c.oid::regclass, a.attnum, false)
WHEN nco.nspname IS NOT NULL THEN current_database() )::bool is_updatable
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 FROM pg_attribute a
LEFT JOIN key_columns kc ON kc.conkey = a.attnum AND kc.c_oid = a.attrelid LEFT JOIN key_columns kc
LEFT JOIN pg_catalog.pg_description AS d ON d.objoid = a.attrelid and d.objsubid = a.attnum ON kc.conkey = a.attnum AND kc.c_oid = a.attrelid
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum LEFT JOIN pg_catalog.pg_description AS d
JOIN (pg_class c ON d.objoid = a.attrelid and d.objsubid = a.attnum
JOIN pg_namespace nc ON c.relnamespace = nc.oid) ON a.attrelid = c.oid LEFT JOIN pg_attrdef ad
JOIN (pg_type t ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
JOIN pg_namespace nt ON t.typnamespace = nt.oid) ON a.atttypid = t.oid JOIN (pg_class c JOIN pg_namespace nc ON c.relnamespace = nc.oid)
LEFT JOIN (pg_type bt ON a.attrelid = c.oid
JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid) ON t.typtype = 'd'::"char" AND t.typbasetype = bt.oid JOIN (pg_type t JOIN pg_namespace nt ON t.typnamespace = nt.oid)
LEFT JOIN (pg_collation co ON a.atttypid = t.oid
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) LEFT JOIN (pg_type bt JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid)
ON t.typtype = 'd' 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 WHERE
NOT pg_is_other_temp_schema(nc.oid) NOT pg_is_other_temp_schema(nc.oid)
AND a.attnum > 0 AND a.attnum > 0
AND NOT a.attisdropped AND NOT a.attisdropped
AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char", 'm'::"char"])) AND c.relkind in ('r', 'v', 'f', 'm')
AND (nc.nspname = $1 OR kc.r_oid IS NOT NULL) /*--filter only columns that are FK/PK or in the api schema */ -- 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))*/ AND (nc.nspname = ANY ($1) OR kc.r_oid IS NOT NULL)
) )
SELECT SELECT
table_schema, table_schema,
@@ -542,7 +520,6 @@ allColumns tabs =
numeric_precision, numeric_precision,
column_default, column_default,
udt_name udt_name
/*-- FROM information_schema.columns*/
FROM columns FROM columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema') WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
) AS info ) AS info
@@ -571,39 +548,36 @@ columnFromRow tabs (s, t, n, desc, pos, nul, typ, u, l, p, d, e) = buildColumn <
parseEnum :: Maybe Text -> [Text] parseEnum :: Maybe Text -> [Text]
parseEnum = maybe [] (split (==',')) parseEnum = maybe [] (split (==','))
allChildRelations :: [Table] -> [Column] -> H.Statement () [Relation] allM2ORels :: [Table] -> [Column] -> H.Statement () [Relation]
allChildRelations tabs cols = allM2ORels tabs cols =
H.Statement sql HE.noParams (decodeRelations tabs cols) True H.Statement sql HE.noParams (decodeRels tabs cols) True
where where
sql = [q| sql = [q|
SELECT ns1.nspname AS table_schema, SELECT ns1.nspname AS table_schema,
tab.relname AS table_name, tab.relname AS table_name,
conname AS constraint_name,
column_info.cols AS columns, column_info.cols AS columns,
ns2.nspname AS foreign_table_schema, ns2.nspname AS foreign_table_schema,
other.relname AS foreign_table_name, other.relname AS foreign_table_name,
column_info.refs AS foreign_columns column_info.refs AS foreign_columns
FROM pg_constraint, FROM pg_constraint,
LATERAL (SELECT array_agg(cols.attname) AS cols, LATERAL (
array_agg(cols.attnum) AS nums, SELECT array_agg(cols.attname) AS cols,
array_agg(refs.attname) AS refs array_agg(cols.attnum) AS nums,
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k, array_agg(refs.attname) AS refs
LATERAL (SELECT * FROM pg_attribute FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
WHERE attrelid = conrelid AND attnum = col) LATERAL (SELECT * FROM pg_attribute WHERE attrelid = conrelid AND attnum = col) AS cols,
AS cols, LATERAL (SELECT * FROM pg_attribute WHERE attrelid = confrelid AND attnum = ref) AS refs) AS column_info,
LATERAL (SELECT * FROM pg_attribute LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1,
WHERE attrelid = confrelid AND attnum = ref) LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab,
AS refs) LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other,
AS column_info, LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2
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 WHERE confrelid != 0
ORDER BY (conrelid, column_info.nums) |] ORDER BY (conrelid, column_info.nums) |]
relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation relFromRow :: [Table] -> [Column] -> (Text, Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) = relFromRow allTabs allCols (rs, rt, cn, rcs, frs, frt, frcs) =
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure Child <*> pure Nothing <*> pure Nothing <*> pure Nothing Relation <$> table <*> cols <*> pure (Just cn) <*> tableF <*> colsF <*> pure M2O <*> pure Nothing
where where
findTable s t = find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs 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 findCol s t c = find (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col == c) allCols
@@ -617,69 +591,36 @@ allPrimaryKeys tabs =
H.Statement sql HE.noParams (decodePks tabs) True H.Statement sql HE.noParams (decodePks tabs) True
where where
sql = [q| sql = [q|
/*
-- CTE to replace information_schema.table_constraints to remove owner limit -- CTE to replace information_schema.table_constraints to remove owner limit
*/
WITH tc AS ( WITH tc AS (
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog, SELECT
nc.nspname::information_schema.sql_identifier AS constraint_schema, c.conname::name AS constraint_name,
c.conname::information_schema.sql_identifier AS constraint_name, nr.nspname::name AS table_schema,
current_database()::information_schema.sql_identifier AS table_catalog, r.relname::name AS table_name
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, FROM pg_namespace nc,
pg_namespace nr, pg_namespace nr,
pg_constraint c, pg_constraint c,
pg_class r 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) WHERE
/*--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))*/ nc.oid = c.connamespace
UNION ALL AND nr.oid = r.relnamespace
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog, AND c.conrelid = r.oid
nr.nspname::information_schema.sql_identifier AS constraint_schema, AND r.relkind = 'r'
(((((nr.oid::text || '_'::text) || r.oid::text) || '_'::text) || a.attnum::text) || '_not_null'::text)::information_schema.sql_identifier AS constraint_name, AND NOT pg_is_other_temp_schema(nr.oid)
current_database()::information_schema.sql_identifier AS table_catalog, AND c.contype = 'p'
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 -- CTE to replace information_schema.key_column_usage to remove owner limit
*/
kc AS ( kc AS (
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog, SELECT
ss.nc_nspname::information_schema.sql_identifier AS constraint_schema, ss.conname::name AS constraint_name,
ss.conname::information_schema.sql_identifier AS constraint_name, ss.nr_nspname::name AS table_schema,
current_database()::information_schema.sql_identifier AS table_catalog, ss.relname::name AS table_name,
ss.nr_nspname::information_schema.sql_identifier AS table_schema, a.attname::name AS column_name,
ss.relname::information_schema.sql_identifier AS table_name, (ss.x).n::integer AS ordinal_position,
a.attname::information_schema.sql_identifier AS column_name, CASE
(ss.x).n::information_schema.cardinal_number AS ordinal_position, WHEN ss.contype = 'f' THEN information_schema._pg_index_position(ss.conindid, ss.confkey[(ss.x).n])
CASE ELSE NULL::integer
WHEN ss.contype = 'f'::"char" THEN information_schema._pg_index_position(ss.conindid, ss.confkey[(ss.x).n]) END::integer AS position_in_unique_constraint
ELSE NULL::integer
END::information_schema.cardinal_number AS position_in_unique_constraint
FROM pg_attribute a, FROM pg_attribute a,
( SELECT r.oid AS roid, ( SELECT r.oid AS roid,
r.relname, r.relname,
@@ -691,28 +632,31 @@ allPrimaryKeys tabs =
c.contype, c.contype,
c.conindid, c.conindid,
c.confkey, c.confkey,
c.confrelid,
information_schema._pg_expandarray(c.conkey) AS x information_schema._pg_expandarray(c.conkey) AS x
FROM pg_namespace nr, FROM pg_namespace nr,
pg_class r, pg_class r,
pg_namespace nc, pg_namespace nc,
pg_constraint c 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
WHERE ss.roid = a.attrelid AND a.attnum = (ss.x).x AND NOT a.attisdropped nr.oid = r.relnamespace
/*--AND (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/ AND r.oid = c.conrelid
AND nc.oid = c.connamespace
AND c.contype in ('p', 'u', 'f')
AND r.relkind = 'r'
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
) )
SELECT SELECT
kc.table_schema, kc.table_schema,
kc.table_name, kc.table_name,
kc.column_name kc.column_name
FROM FROM
/*
--information_schema.table_constraints tc,
--information_schema.key_column_usage kc
*/
tc, kc tc, kc
WHERE WHERE
tc.constraint_type = 'PRIMARY KEY' AND
kc.table_name = tc.table_name AND kc.table_name = tc.table_name AND
kc.table_schema = tc.table_schema AND kc.table_schema = tc.table_schema AND
kc.constraint_name = tc.constraint_name AND kc.constraint_name = tc.constraint_name AND
@@ -722,9 +666,9 @@ pkFromRow :: [Table] -> (Schema, Text, Text) -> Maybe PrimaryKey
pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n
where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
allSynonyms :: [Column] -> PgVersion -> H.Statement Schema [Synonym] allSourceColumns :: [Column] -> PgVersion -> H.Statement [Schema] [SourceColumn]
allSynonyms cols pgVer = allSourceColumns cols pgVer =
H.Statement sql (param HE.text) (decodeSynonyms cols) True H.Statement sql (arrayParam HE.text) (decodeSourceColumns cols) True
-- query explanation at https://gist.github.com/steve-chavez/7ee0e6590cddafb532e5f00c46275569 -- query explanation at https://gist.github.com/steve-chavez/7ee0e6590cddafb532e5f00c46275569
where where
subselectRegex :: Text subselectRegex :: Text
@@ -743,7 +687,7 @@ allSynonyms cols pgVer =
from pg_class c from pg_class c
join pg_namespace n on n.oid = c.relnamespace join pg_namespace n on n.oid = c.relnamespace
join pg_rewrite r on r.ev_class = c.oid join pg_rewrite r on r.ev_class = c.oid
where (c.relkind in ('v', 'm')) and n.nspname = $1 where c.relkind in ('v', 'm') and n.nspname = ANY ($1)
), ),
removed_subselects as( removed_subselects as(
select select
@@ -791,13 +735,6 @@ allSynonyms cols pgVer =
where resorigtbl <> '0' where resorigtbl <> '0'
order by view_schema, view_name, view_colum_name; |] 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.Session PgVersion
getPgVersion = H.statement () $ H.Statement sql HE.noParams versionRow False getPgVersion = H.statement () $ H.Statement sql HE.noParams versionRow False
where where
+48 -26
View File
@@ -16,19 +16,19 @@ module PostgREST.Error (
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.Text as T
import qualified Hasql.Pool as P import qualified Hasql.Pool as P
import qualified Hasql.Session as H import qualified Hasql.Session as H
import qualified Network.HTTP.Types.Status as HT import qualified Network.HTTP.Types.Status as HT
import Data.Aeson ((.=)) import Data.Aeson ((.=))
import Data.Text (unwords)
import Network.Wai (Response, responseLBS) import Network.Wai (Response, responseLBS)
import Text.Read (readMaybe)
import Network.HTTP.Types.Header import Network.HTTP.Types.Header
import PostgREST.Types import PostgREST.Types
import Protolude import Protolude hiding (toS)
import Protolude.Conv (toS)
class (JSON.ToJSON a) => PgrstError a where class (JSON.ToJSON a) => PgrstError a where
@@ -48,8 +48,10 @@ data ApiRequestError
| InvalidRange | InvalidRange
| InvalidBody ByteString | InvalidBody ByteString
| ParseRequestError Text Text | ParseRequestError Text Text
| NoRelationBetween Text Text | NoRelBetween Text Text
| AmbiguousRelBetween Text Text [Relation]
| InvalidFilters | InvalidFilters
| UnacceptableSchema [Text]
| UnknownRelation -- Unreachable? | UnknownRelation -- Unreachable?
| UnsupportedVerb -- Unreachable? | UnsupportedVerb -- Unreachable?
deriving (Show, Eq) deriving (Show, Eq)
@@ -62,7 +64,9 @@ instance PgrstError ApiRequestError where
status UnknownRelation = HT.status404 status UnknownRelation = HT.status404
status ActionInappropriate = HT.status405 status ActionInappropriate = HT.status405
status (ParseRequestError _ _) = HT.status400 status (ParseRequestError _ _) = HT.status400
status (NoRelationBetween _ _) = HT.status400 status (NoRelBetween _ _) = HT.status400
status AmbiguousRelBetween{} = HT.status300
status (UnacceptableSchema _) = HT.status406
headers _ = [toHeader CTApplicationJSON] headers _ = [toHeader CTApplicationJSON]
@@ -77,13 +81,39 @@ instance JSON.ToJSON ApiRequestError where
"message" .= ("HTTP Range error" :: Text)] "message" .= ("HTTP Range error" :: Text)]
toJSON UnknownRelation = JSON.object [ toJSON UnknownRelation = JSON.object [
"message" .= ("Unknown relation" :: Text)] "message" .= ("Unknown relation" :: Text)]
toJSON (NoRelationBetween parent child) = JSON.object [ toJSON (NoRelBetween parent child) = JSON.object [
"message" .= ("Could not find foreign keys between these entities, No relation found between " <> parent <> " and " <> child :: Text)] "message" .= ("Could not find foreign keys between these entities. No relationship found between " <> parent <> " and " <> child :: Text)]
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
"hint" .= ("By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)" :: Text),
"message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text),
"details" .= (compressedRel <$> rels) ]
toJSON UnsupportedVerb = JSON.object [ toJSON UnsupportedVerb = JSON.object [
"message" .= ("Unsupported HTTP verb" :: Text)] "message" .= ("Unsupported HTTP verb" :: Text)]
toJSON InvalidFilters = JSON.object [ toJSON InvalidFilters = JSON.object [
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text)] "message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text)]
toJSON (UnacceptableSchema schemas) = JSON.object [
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas)]
compressedRel :: Relation -> JSON.Value
compressedRel rel =
let
fmtTbl tbl = tableSchema tbl <> "." <> tableName tbl
fmtEls els = "[" <> T.intercalate ", " els <> "]"
in
JSON.object $ [
"origin" .= fmtTbl (relTable rel)
, "target" .= fmtTbl (relFTable rel)
, "cardinality" .= (show $ relType rel :: Text)
] ++
case (relType rel, relJunction rel, relConstraint rel) of
(M2M, Just (Junction jt (Just const1) _ (Just const2) _), _) -> [
"relationship" .= (fmtTbl jt <> fmtEls [const1] <> fmtEls [const2])
]
(_, _, Just relCon) -> [
"relationship" .= (relCon <> fmtEls (colName <$> relColumns rel) <> fmtEls (colName <$> relFColumns rel))
]
(_, _, _) ->
mempty
data PgError = PgError Authenticated P.UsageError data PgError = PgError Authenticated P.UsageError
type Authenticated = Bool type Authenticated = Bool
@@ -193,10 +223,8 @@ data SimpleError
= GucHeadersError = GucHeadersError
| BinaryFieldError ContentType | BinaryFieldError ContentType
| ConnectionLostError | ConnectionLostError
| PutSingletonError
| PutMatchingPkError | PutMatchingPkError
| PutRangeNotAllowedError | PutRangeNotAllowedError
| PutPayloadIncompleteError
| JwtTokenMissing | JwtTokenMissing
| JwtTokenInvalid Text | JwtTokenInvalid Text
| SingularityError Integer | SingularityError Integer
@@ -204,17 +232,15 @@ data SimpleError
deriving (Show, Eq) deriving (Show, Eq)
instance PgrstError SimpleError where instance PgrstError SimpleError where
status GucHeadersError = HT.status500 status GucHeadersError = HT.status500
status (BinaryFieldError _) = HT.status406 status (BinaryFieldError _) = HT.status406
status ConnectionLostError = HT.status503 status ConnectionLostError = HT.status503
status PutSingletonError = HT.status400 status PutMatchingPkError = HT.status400
status PutMatchingPkError = HT.status400 status PutRangeNotAllowedError = HT.status400
status PutRangeNotAllowedError = HT.status400 status JwtTokenMissing = HT.status500
status PutPayloadIncompleteError = HT.status400 status (JwtTokenInvalid _) = HT.unauthorized401
status JwtTokenMissing = HT.status500 status (SingularityError _) = HT.status406
status (JwtTokenInvalid _) = HT.unauthorized401 status (ContentTypeError _) = HT.status415
status (SingularityError _) = HT.status406
status (ContentTypeError _) = HT.status415
headers (SingularityError _) = [toHeader CTSingularJSON] headers (SingularityError _) = [toHeader CTSingularJSON]
headers (JwtTokenInvalid m) = [toHeader CTApplicationJSON, invalidTokenHeader m] headers (JwtTokenInvalid m) = [toHeader CTApplicationJSON, invalidTokenHeader m]
@@ -228,12 +254,8 @@ instance JSON.ToJSON SimpleError where
toJSON ConnectionLostError = JSON.object [ toJSON ConnectionLostError = JSON.object [
"message" .= ("Database connection lost, retrying the connection." :: Text)] "message" .= ("Database connection lost, retrying the connection." :: Text)]
toJSON PutSingletonError = JSON.object [
"message" .= ("PUT payload must contain a single row" :: Text)]
toJSON PutRangeNotAllowedError = JSON.object [ toJSON PutRangeNotAllowedError = JSON.object [
"message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text)] "message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text)]
toJSON PutPayloadIncompleteError = JSON.object [
"message" .= ("You must specify all columns in the payload when using PUT" :: Text)]
toJSON PutMatchingPkError = JSON.object [ toJSON PutMatchingPkError = JSON.object [
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text)] "message" .= ("Payload values do not match URL in primary key column(s)" :: Text)]
@@ -241,7 +263,7 @@ instance JSON.ToJSON SimpleError where
"message" .= ("None of these Content-Types are available: " <> (toS . intercalate ", " . map toS) cts :: Text)] "message" .= ("None of these Content-Types are available: " <> (toS . intercalate ", " . map toS) cts :: Text)]
toJSON (SingularityError n) = JSON.object [ toJSON (SingularityError n) = JSON.object [
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text), "message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
"details" .= unwords ["Results contain", show n, "rows,", toS (toMime CTSingularJSON), "requires 1 row"]] "details" .= T.unwords ["Results contain", show n, "rows,", toS (toMime CTSingularJSON), "requires 1 row"]]
toJSON JwtTokenMissing = JSON.object [ toJSON JwtTokenMissing = JSON.object [
"message" .= ("Server lacks JWT secret" :: Text)] "message" .= ("Server lacks JWT secret" :: Text)]
@@ -250,7 +272,7 @@ instance JSON.ToJSON SimpleError where
invalidTokenHeader :: Text -> Header invalidTokenHeader :: Text -> Header
invalidTokenHeader m = invalidTokenHeader m =
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> show m) ("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> encodeUtf8 (show m))
singularityError :: (Integral a) => a -> SimpleError singularityError :: (Integral a) => a -> SimpleError
singularityError = SingularityError . toInteger singularityError = SingularityError . toInteger
+21 -10
View File
@@ -10,6 +10,8 @@ module PostgREST.Middleware where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import Data.Scientific (FPFormat (..), formatScientific,
isInteger)
import qualified Hasql.Transaction as H import qualified Hasql.Transaction as H
import Network.Wai (Application, Response) import Network.Wai (Application, Response)
@@ -24,9 +26,9 @@ import PostgREST.Auth (JWTAttempt (..))
import PostgREST.Config (AppConfig (..), corsPolicy) import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (SimpleError (JwtTokenInvalid, JwtTokenMissing), import PostgREST.Error (SimpleError (JwtTokenInvalid, JwtTokenMissing),
errorResponseFor) errorResponseFor)
import PostgREST.QueryBuilder (pgFmtSetLocal, pgFmtSetLocalSearchPath, import PostgREST.QueryBuilder (setLocalQuery, setLocalSearchPathQuery)
unquoted) import Protolude hiding (head, toS)
import Protolude import Protolude.Conv (toS)
runWithClaims :: AppConfig -> JWTAttempt -> runWithClaims :: AppConfig -> JWTAttempt ->
(ApiRequest -> H.Transaction Response) -> (ApiRequest -> H.Transaction Response) ->
@@ -37,17 +39,19 @@ runWithClaims conf eClaims app req =
JWTInvalid JWTExpired -> return . errorResponseFor . JwtTokenInvalid $ "JWT expired" JWTInvalid JWTExpired -> return . errorResponseFor . JwtTokenInvalid $ "JWT expired"
JWTInvalid e -> return . errorResponseFor . JwtTokenInvalid . show $ e JWTInvalid e -> return . errorResponseFor . JwtTokenInvalid . show $ e
JWTClaims claims -> do JWTClaims claims -> do
H.sql $ toS . mconcat $ setSearchPathSql : setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql ++ appSettingsSql H.sql $ toS . mconcat $ setSearchPathSql : setRoleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql
mapM_ H.sql customReqCheck mapM_ H.sql customReqCheck
app req app req
where where
headersSql = pgFmtSetLocal "request.header." <$> iHeaders req methodSql = setLocalQuery mempty ("request.method", toS $ iMethod req)
cookiesSql = pgFmtSetLocal "request.cookie." <$> iCookies req pathSql = setLocalQuery mempty ("request.path", toS $ iPath req)
claimsSql = pgFmtSetLocal "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole] headersSql = setLocalQuery "request.header." <$> iHeaders req
appSettingsSql = pgFmtSetLocal mempty <$> configSettings conf cookiesSql = setLocalQuery "request.cookie." <$> iCookies req
claimsSql = setLocalQuery "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
appSettingsSql = setLocalQuery mempty <$> configSettings conf
setRoleSql = maybeToList $ (\x -> setRoleSql = maybeToList $ (\x ->
pgFmtSetLocal mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole setLocalQuery mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole
setSearchPathSql = pgFmtSetLocalSearchPath $ configSchema conf : configExtraSearchPath conf setSearchPathSql = setLocalSearchPathQuery (iSchema req : configExtraSearchPath conf)
-- role claim defaults to anon if not specified in jwt -- role claim defaults to anon if not specified in jwt
claimsWithRole = M.union claims (M.singleton "role" anon) claimsWithRole = M.union claims (M.singleton "role" anon)
anon = JSON.String . toS $ configAnonRole conf anon = JSON.String . toS $ configAnonRole conf
@@ -58,3 +62,10 @@ defaultMiddle =
gzip def gzip def
. cors corsPolicy . cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")]) . staticPolicy (only [("favicon.ico", "static/favicon.ico")])
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
+25 -17
View File
@@ -10,7 +10,7 @@ module PostgREST.OpenAPI (
, pickProxy , pickProxy
) where ) where
import qualified Data.Set as Set import qualified Data.HashSet.InsOrd as Set
import Control.Arrow ((&&&)) import Control.Arrow ((&&&))
import Data.Aeson (decode, encode) import Data.Aeson (decode, encode)
@@ -32,7 +32,8 @@ import PostgREST.Types (Column (..), ForeignKey (..), PgArg (..),
PrimaryKey (..), ProcDescription (..), PrimaryKey (..), ProcDescription (..),
Proxy (..), Table (..), toMime) Proxy (..), Table (..), toMime)
import Protolude hiding (Proxy, dropWhile, get, import Protolude hiding (Proxy, dropWhile, get,
intercalate, (&)) intercalate, toLower, toS, (&))
import Protolude.Conv (toS)
makeMimeList :: [ContentType] -> MimeList makeMimeList :: [ContentType] -> MimeList
makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs
@@ -55,7 +56,7 @@ makeTableDef pks (t, cs, _) =
let tn = tableName t in let tn = tableName t in
(tn, (mempty :: Schema) (tn, (mempty :: Schema)
& description .~ tableDescription t & description .~ tableDescription t
& type_ .~ SwaggerObject & type_ ?~ SwaggerObject
& properties .~ fromList (map (makeProperty pks) cs) & properties .~ fromList (map (makeProperty pks) cs)
& required .~ map colName (filter (not . colNullable) cs)) & required .~ map colName (filter (not . colNullable) cs))
@@ -84,13 +85,13 @@ makeProperty pks c = (colName c, Inline s)
& enum_ .~ e & enum_ .~ e
& format ?~ colType c & format ?~ colType c
& maxLength .~ (fromIntegral <$> colMaxLen c) & maxLength .~ (fromIntegral <$> colMaxLen c)
& type_ .~ toSwaggerType (colType c) & type_ ?~ toSwaggerType (colType c)
makeProcSchema :: ProcDescription -> Schema makeProcSchema :: ProcDescription -> Schema
makeProcSchema pd = makeProcSchema pd =
(mempty :: Schema) (mempty :: Schema)
& description .~ pdDescription pd & description .~ pdDescription pd
& type_ .~ SwaggerObject & type_ ?~ SwaggerObject
& properties .~ fromList (map makeProcProperty (pdArgs pd)) & properties .~ fromList (map makeProcProperty (pdArgs pd))
& required .~ map pgaName (filter pgaReq (pdArgs pd)) & required .~ map pgaName (filter pgaReq (pdArgs pd))
@@ -98,7 +99,7 @@ makeProcProperty :: PgArg -> (Text, Referenced Schema)
makeProcProperty (PgArg n t _) = (n, Inline s) makeProcProperty (PgArg n t _) = (n, Inline s)
where where
s = (mempty :: Schema) s = (mempty :: Schema)
& type_ .~ toSwaggerType t & type_ ?~ toSwaggerType t
& format ?~ t & format ?~ t
makePreferParam :: [Text] -> Param makePreferParam :: [Text] -> Param
@@ -109,7 +110,7 @@ makePreferParam ts =
& required ?~ False & required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader & in_ .~ ParamHeader
& type_ .~ SwaggerString & type_ ?~ SwaggerString
& enum_ .~ decode (encode ts)) & enum_ .~ decode (encode ts))
makeProcParam :: ProcDescription -> [Referenced Param] makeProcParam :: ProcDescription -> [Referenced Param]
@@ -117,7 +118,7 @@ makeProcParam pd =
[ Inline $ (mempty :: Param) [ Inline $ (mempty :: Param)
& name .~ "args" & name .~ "args"
& required ?~ True & required ?~ True
& schema .~ (ParamBody $ Inline $ makeProcSchema pd) & schema .~ ParamBody (Inline $ makeProcSchema pd)
, Ref $ Reference "preferParams" , Ref $ Reference "preferParams"
] ]
@@ -132,28 +133,35 @@ makeParamDefs ti =
& required ?~ False & required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery & in_ .~ ParamQuery
& type_ .~ SwaggerString)) & type_ ?~ SwaggerString))
, ("on_conflict", (mempty :: Param)
& name .~ "on_conflict"
& description ?~ "On Conflict"
& required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery
& type_ ?~ SwaggerString))
, ("order", (mempty :: Param) , ("order", (mempty :: Param)
& name .~ "order" & name .~ "order"
& description ?~ "Ordering" & description ?~ "Ordering"
& required ?~ False & required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery & in_ .~ ParamQuery
& type_ .~ SwaggerString)) & type_ ?~ SwaggerString))
, ("range", (mempty :: Param) , ("range", (mempty :: Param)
& name .~ "Range" & name .~ "Range"
& description ?~ "Limiting and Pagination" & description ?~ "Limiting and Pagination"
& required ?~ False & required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader & in_ .~ ParamHeader
& type_ .~ SwaggerString)) & type_ ?~ SwaggerString))
, ("rangeUnit", (mempty :: Param) , ("rangeUnit", (mempty :: Param)
& name .~ "Range-Unit" & name .~ "Range-Unit"
& description ?~ "Limiting and Pagination" & description ?~ "Limiting and Pagination"
& required ?~ False & required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader & in_ .~ ParamHeader
& type_ .~ SwaggerString & type_ ?~ SwaggerString
& default_ .~ decode "\"items\"")) & default_ .~ decode "\"items\""))
, ("offset", (mempty :: Param) , ("offset", (mempty :: Param)
& name .~ "offset" & name .~ "offset"
@@ -161,14 +169,14 @@ makeParamDefs ti =
& required ?~ False & required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery & in_ .~ ParamQuery
& type_ .~ SwaggerString)) & type_ ?~ SwaggerString))
, ("limit", (mempty :: Param) , ("limit", (mempty :: Param)
& name .~ "limit" & name .~ "limit"
& description ?~ "Limiting and Pagination" & description ?~ "Limiting and Pagination"
& required ?~ False & required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery & in_ .~ ParamQuery
& type_ .~ SwaggerString)) & type_ ?~ SwaggerString))
] ]
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) cs <> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) cs
| (t, cs, _) <- ti | (t, cs, _) <- ti
@@ -190,7 +198,7 @@ makeRowFilter tn c =
& required ?~ False & required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery & in_ .~ ParamQuery
& type_ .~ SwaggerString & type_ ?~ SwaggerString
& format ?~ colType c)) & format ?~ colType c))
makeRowFilters :: Text -> [Column] -> [(Text, Param)] makeRowFilters :: Text -> [Column] -> [(Text, Param)]
@@ -213,8 +221,8 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
& at 200 ?~ Inline ((mempty :: Response) & at 200 ?~ Inline ((mempty :: Response)
& description .~ "OK" & description .~ "OK"
& schema ?~ Inline (mempty & schema ?~ Inline (mempty
& type_ .~ SwaggerArray & type_ ?~ SwaggerArray
& items ?~ (SwaggerItemsObject $ Ref $ Reference $ tableName t) & items ?~ SwaggerItemsObject (Ref $ Reference $ tableName t)
) )
) )
postOp = tOp postOp = tOp
+12 -8
View File
@@ -9,10 +9,8 @@ module PostgREST.Parsers where
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import qualified Data.Set as S import qualified Data.Set as S
import Control.Monad ((>>))
import Data.Either.Combinators (mapLeft) import Data.Either.Combinators (mapLeft)
import Data.Foldable (foldl1) import Data.Foldable (foldl1)
import Data.Functor (($>))
import Data.List (init, last) import Data.List (init, last)
import Data.Text (intercalate, replace, strip) import Data.Text (intercalate, replace, strip)
import Text.Read (read) import Text.Read (read)
@@ -24,12 +22,18 @@ import Text.ParserCombinators.Parsec hiding (many, (<|>))
import PostgREST.Error (ApiRequestError (ParseRequestError)) import PostgREST.Error (ApiRequestError (ParseRequestError))
import PostgREST.RangeQuery (NonnegRange) import PostgREST.RangeQuery (NonnegRange)
import PostgREST.Types import PostgREST.Types
import Protolude hiding (intercalate, option, replace, try) import Protolude hiding (intercalate, option, replace, toS,
try)
import Protolude.Conv (toS)
pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem] pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem]
pRequestSelect selStr = pRequestSelect selStr =
mapError $ parse pFieldForest ("failed to parse select parameter (" <> toS selStr <> ")") (toS selStr) mapError $ parse pFieldForest ("failed to parse select parameter (" <> toS selStr <> ")") (toS selStr)
pRequestOnConflict :: Text -> Either ApiRequestError [FieldName]
pRequestOnConflict oncStr =
mapError $ parse pColumns ("failed to parse on_conflict parameter (" <> toS oncStr <> ")") (toS oncStr)
pRequestFilter :: (Text, Text) -> Either ApiRequestError (EmbedPath, Filter) pRequestFilter :: (Text, Text) -> Either ApiRequestError (EmbedPath, Filter)
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper) pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
where where
@@ -130,12 +134,12 @@ pRelationSelect :: Parser SelectItem
pRelationSelect = lexeme $ try ( do pRelationSelect = lexeme $ try ( do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) ) alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField fld <- pField
relationDetail <- optionMaybe ( hint <- optionMaybe (
try ( char '!' *> pFieldName ) <|> try ( char '!' *> pFieldName) <|>
try ( char '.' *> pFieldName ) -- TODO deprecated, remove in next major version -- deprecated, remove in next major version
try ( char '.' *> pFieldName)
) )
return (fld, Nothing, alias, hint)
return (fld, Nothing, alias, relationDetail)
) )
pFieldSelect :: Parser SelectItem pFieldSelect :: Parser SelectItem
+25
View File
@@ -0,0 +1,25 @@
{-|
Module : PostgREST.Common
Description : Common helper functions.
-}
module PostgREST.Private.Common where
import Data.Maybe
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import Protolude
column :: HD.Value a -> HD.Row a
column = HD.column . HD.nonNullable
nullableColumn :: HD.Value a -> HD.Row (Maybe a)
nullableColumn = HD.column . HD.nullable
element :: HD.Value a -> HD.Array a
element = HD.element . HD.nonNullable
param :: HE.Value a -> HE.Params a
param = HE.param . HE.nonNullable
arrayParam :: HE.Value a -> HE.Params [a]
arrayParam = param . HE.array . HE.dimension foldl' . HE.element . HE.nonNullable
@@ -1,75 +1,34 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-| {-|
Module : PostgREST.QueryBuilder.Private Module : PostgREST.Private.QueryFragment
Description : Helper functions for PostgREST.QueryBuilder. Description : Helper functions for PostgREST.QueryBuilder.
-}
module PostgREST.QueryBuilder.Private where
import qualified Data.ByteString.Char8 as BS Any function that outputs a SqlFragment should be in this module.
-}
module PostgREST.Private.QueryFragment where
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import Data.Maybe import Data.Maybe
import Data.Text (intercalate, import Data.Text (intercalate,
isInfixOf, replace, isInfixOf, replace,
toLower, unwords) toLower)
import qualified Data.Text as T (map, null, import qualified Data.Text as T (map, null,
takeWhile) takeWhile)
import qualified Data.Text.Encoding as T
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as H
import PostgREST.Types import PostgREST.Types
import Protolude hiding (cast, import Protolude hiding (cast,
intercalate, replace) intercalate, replace,
toLower)
import Text.InterpolatedString.Perl6 (qc) import Text.InterpolatedString.Perl6 (qc)
column :: HD.Value a -> HD.Row a noLocationF :: SqlFragment
column = HD.column . HD.nonNullable
nullableColumn :: HD.Value a -> HD.Row (Maybe a)
nullableColumn = HD.column . HD.nullable
element :: HD.Value a -> HD.Array a
element = HD.element . HD.nonNullable
param :: HE.Value a -> HE.Params a
param = HE.param . HE.nonNullable
{-| 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 = (,,,) <$> nullableColumn HD.int8 <*> column HD.int8
<*> column header <*> column HD.bytea
where
header = HD.array $ HD.dimension replicateM $ element HD.bytea
noLocationF :: Text
noLocationF = "array[]::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
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then "" else schema) tbl
-- Due to the use of the `unknown` encoder we need to cast '$1' when the value is not used in the main query -- 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`. -- 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. -- This happens because `unknown` relies on the context to determine the value type.
-- The error also happens on raw libpq used with C. -- The error also happens on raw libpq used with C.
ignoredBody :: SqlFragment ignoredBody :: SqlFragment
ignoredBody = "ignored_body AS (SELECT $1::text) " ignoredBody = "pgrst_ignored_body AS (SELECT $1::text) "
-- | -- |
-- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads -- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads
@@ -139,9 +98,6 @@ fromQi t = (if s == "" then "" else pgFmtIdent s <> ".") <> pgFmtIdent n
n = qiName t n = qiName t
s = qiSchema 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 :: Text -> Bool -> Text
emptyOnFalse val cond = if cond then "" else val emptyOnFalse val cond = if cond then "" else val
@@ -195,9 +151,8 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
(find ((==) . toLower $ v) ["null","true","false"]) (find ((==) . toLower $ v) ["null","true","false"])
pgFmtJoinCondition :: JoinCondition -> SqlFragment pgFmtJoinCondition :: JoinCondition -> SqlFragment
pgFmtJoinCondition (JoinCondition (qi, col1) (QualifiedIdentifier schema fTable, col2)) = pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
pgFmtColumn qi col1 <> " = " <> pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
pgFmtColumn (removeSourceCTESchema schema fTable) col2
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment
pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <> show op <> " ") (pgFmtLogicTree qi <$> forest) <> ")" pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <> show op <> " ") (pgFmtLogicTree qi <$> forest) <> ")"
@@ -225,13 +180,27 @@ pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of
Nothing -> "" Nothing -> ""
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias 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 :: Text -> Text
trimNullChars = T.takeWhile (/= '\x0') trimNullChars = T.takeWhile (/= '\x0')
countF :: SqlQuery -> Bool -> (SqlFragment, SqlFragment)
countF countQuery shouldCount =
if shouldCount
then (
", pgrst_source_count AS (" <> countQuery <> ")"
, "(SELECT pg_catalog.count(*) FROM pgrst_source_count)" )
else (
mempty
, "null::bigint")
returningF :: QualifiedIdentifier -> [FieldName] -> SqlFragment
returningF qi returnings =
if null returnings
then "RETURNING 1" -- For mutation cases where there's no ?select, we return 1 to know how many rows were modified
else "RETURNING " <> intercalate ", " (pgFmtColumn qi <$> returnings)
responseHeadersF :: PgVersion -> SqlFragment
responseHeadersF pgVer =
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
+136 -96
View File
@@ -4,58 +4,38 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-| {-|
Module : PostgREST.QueryBuilder Module : PostgREST.QueryBuilder
Description : PostgREST SQL generating functions. Description : PostgREST SQL queries generating functions.
This module provides functions to consume data types that This module provides functions to consume data types that
represent database objects (e.g. Relation, Schema, SqlQuery) represent database queries (e.g. ReadRequest, MutateRequest) and SqlFragment
and produces SQL Statements. to produce SqlQuery type outputs.
Any function that outputs a SQL fragment should be in this module.
-} -}
module PostgREST.QueryBuilder ( module PostgREST.QueryBuilder (
callProc readRequestToQuery
, createReadStatement , mutateRequestToQuery
, createWriteStatement , readRequestToCountQuery
, requestToQuery , requestToCallProcQuery
, requestToCountQuery , limitedQuery
, unquoted , setLocalQuery
, ResultsWithCount , setLocalSearchPathQuery
, pgFmtSetLocal
, pgFmtSetLocalSearchPath
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Set as S
import qualified Data.Set as S
import Data.Scientific (FPFormat (..), formatScientific, isInteger) import Data.Text (intercalate)
import Data.Text (intercalate, unwords) import Data.Tree (Tree (..))
import Data.Tree (Tree (..))
import Data.Maybe import Data.Maybe
import PostgREST.QueryBuilder.Private import PostgREST.Private.QueryFragment
import PostgREST.QueryBuilder.Procedure import PostgREST.RangeQuery (allRange, rangeLimit,
import PostgREST.QueryBuilder.ReadStatement rangeOffset)
import PostgREST.QueryBuilder.WriteStatement
import PostgREST.RangeQuery (allRange, rangeLimit,
rangeOffset)
import PostgREST.Types import PostgREST.Types
import Protolude hiding (cast, import Protolude hiding (cast, intercalate,
intercalate, replace) replace)
requestToCountQuery :: Schema -> DbRequest -> SqlQuery readRequestToQuery :: ReadRequest -> SqlQuery
requestToCountQuery _ (DbMutate _) = witness readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicForest joinConditions_ ordts range, _) forest) =
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 tbl tblAlias implJoins logicForest joinConditions_ ordts range, _) forest)) =
unwords [ unwords [
"SELECT " <> intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects), "SELECT " <> intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects),
"FROM " <> intercalate ", " (tabl : implJs), "FROM " <> intercalate ", " (tabl : implJs),
@@ -63,51 +43,42 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbl tblAlias imp
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConditions_)) ("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConditions_))
`emptyOnFalse` (null logicForest && null joinConditions_), `emptyOnFalse` (null logicForest && null joinConditions_),
("ORDER BY " <> intercalate ", " (map (pgFmtOrderTerm qi) ordts)) `emptyOnFalse` null ordts, ("ORDER BY " <> intercalate ", " (map (pgFmtOrderTerm qi) ordts)) `emptyOnFalse` null ordts,
("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (isParent || range == allRange) ] ("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (range == allRange)
]
where where
implJs = fromQi . QualifiedIdentifier schema <$> implJoins implJs = fromQi <$> implJoins
mainQi = removeSourceCTESchema schema tbl
tabl = fromQi mainQi <> maybe mempty (\a -> " AS " <> pgFmtIdent a) tblAlias tabl = fromQi mainQi <> maybe mempty (\a -> " AS " <> pgFmtIdent a) tblAlias
qi = maybe mainQi (QualifiedIdentifier mempty) tblAlias qi = maybe mainQi (QualifiedIdentifier mempty) tblAlias
(joins, selects) = foldr getJoinsSelects ([],[]) forest
(joins, selects) = foldr getQueryParts ([],[]) forest getJoinsSelects :: ReadRequest -> ([SqlFragment], [SqlFragment]) -> ([SqlFragment], [SqlFragment])
getJoinsSelects rr@(Node (_, (name, Just Relation{relType=relTyp,relTable=Table{tableName=table}}, alias, _, _)) _) (j,s) =
let subquery = readRequestToQuery rr in
case relTyp of
M2O ->
let 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 " in
(joi:j,sel:s)
_ ->
let sel = "COALESCE (("
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias) in
(j,sel:s)
getJoinsSelects (Node (_, (_, Nothing, _, _, _)) _) _ = ([], [])
getQueryParts :: Tree ReadNode -> ([SqlFragment], [SqlFragment]) -> ([SqlFragment], [SqlFragment]) mutateRequestToQuery :: MutateRequest -> SqlQuery
getQueryParts (Node n@(_, (name, Just Relation{relType=Child,relTable=Table{tableName=table}}, alias, _, _)) forst) (j,s) = (j,sel:s) mutateRequestToQuery (Insert mainQi iCols onConflct putConditions returnings) =
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 iCols onConflct putConditions returnings)) =
unwords [ unwords [
"WITH " <> normalizedBody, "WITH " <> normalizedBody,
"INSERT INTO ", fromQi qi, if S.null iCols then " " else "(" <> cols <> ")", "INSERT INTO ", fromQi mainQi, if S.null iCols then " " else "(" <> cols <> ")",
unwords [ unwords [
"SELECT " <> cols <> " FROM", "SELECT " <> cols <> " FROM",
"json_populate_recordset", "(null::", fromQi qi, ", " <> selectBody <> ") _", "json_populate_recordset", "(null::", fromQi mainQi, ", " <> selectBody <> ") _",
-- Only used for PUT -- Only used for PUT
("WHERE " <> intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier "" "_") <$> putConditions)) `emptyOnFalse` null putConditions], ("WHERE " <> intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "_") <$> putConditions)) `emptyOnFalse` null putConditions],
maybe "" (\(oncDo, oncCols) -> ( maybe "" (\(oncDo, oncCols) -> (
"ON CONFLICT(" <> intercalate ", " (pgFmtIdent <$> oncCols) <> ") " <> case oncDo of "ON CONFLICT(" <> intercalate ", " (pgFmtIdent <$> oncCols) <> ") " <> case oncDo of
IgnoreDuplicates -> IgnoreDuplicates ->
@@ -117,37 +88,106 @@ requestToQuery schema _ (DbMutate (Insert mainTbl iCols onConflct putConditions
then "DO NOTHING" then "DO NOTHING"
else "DO UPDATE SET " <> intercalate ", " (pgFmtIdent <> const " = EXCLUDED." <> pgFmtIdent <$> S.toList iCols) else "DO UPDATE SET " <> intercalate ", " (pgFmtIdent <> const " = EXCLUDED." <> pgFmtIdent <$> S.toList iCols)
) `emptyOnFalse` null oncCols) onConflct, ) `emptyOnFalse` null oncCols) onConflct,
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings] returningF mainQi returnings
]
where where
qi = QualifiedIdentifier schema mainTbl
cols = intercalate ", " $ pgFmtIdent <$> S.toList iCols cols = intercalate ", " $ pgFmtIdent <$> S.toList iCols
requestToQuery schema _ (DbMutate (Update mainTbl uCols logicForest returnings)) = mutateRequestToQuery (Update mainQi uCols logicForest returnings) =
if S.null uCols if S.null uCols
then "WITH " <> ignoredBody <> "SELECT null WHERE false" -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
-- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=
-- the select has to be based on "returnings" to make computed overloaded functions not throw
then "WITH " <> ignoredBody <> "SELECT " <> empty_body_returned_columns <> " FROM " <> fromQi mainQi <> " WHERE false"
else else
unwords [ unwords [
"WITH " <> normalizedBody, "WITH " <> normalizedBody,
"UPDATE " <> fromQi qi <> " SET " <> cols, "UPDATE " <> fromQi mainQi <> " SET " <> cols,
"FROM (SELECT * FROM json_populate_recordset", "(null::", fromQi qi, ", " <> selectBody <> ")) _ ", "FROM (SELECT * FROM json_populate_recordset", "(null::", fromQi mainQi, ", " <> selectBody <> ")) _ ",
("WHERE " <> intercalate " AND " (pgFmtLogicTree qi <$> logicForest)) `emptyOnFalse` null logicForest, ("WHERE " <> intercalate " AND " (pgFmtLogicTree mainQi <$> logicForest)) `emptyOnFalse` null logicForest,
("RETURNING " <> intercalate ", " (pgFmtColumn qi <$> returnings)) `emptyOnFalse` null returnings returningF mainQi returnings
] ]
where where
qi = QualifiedIdentifier schema mainTbl
cols = intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols) cols = intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols)
requestToQuery schema _ (DbMutate (Delete mainTbl logicForest returnings)) = empty_body_returned_columns :: SqlFragment
empty_body_returned_columns
| null returnings = "NULL"
| otherwise = intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
mutateRequestToQuery (Delete mainQi logicForest returnings) =
unwords [ unwords [
"WITH " <> ignoredBody, "WITH " <> ignoredBody,
"DELETE FROM ", fromQi qi, "DELETE FROM ", fromQi mainQi,
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest)) `emptyOnFalse` null logicForest, ("WHERE " <> intercalate " AND " (map (pgFmtLogicTree mainQi) logicForest)) `emptyOnFalse` null logicForest,
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings returningF mainQi returnings
] ]
where
qi = QualifiedIdentifier schema mainTbl
unquoted :: JSON.Value -> Text requestToCallProcQuery :: QualifiedIdentifier -> [PgArg] -> Bool -> Maybe PreferParameters -> [FieldName] -> SqlQuery
unquoted (JSON.String t) = t requestToCallProcQuery qi pgArgs returnsScalar preferParams returnings =
unquoted (JSON.Number n) = unwords [
toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n "WITH",
unquoted (JSON.Bool b) = show b argsCTE,
unquoted v = toS $ JSON.encode v sourceBody ]
where
paramsAsSingleObject = preferParams == Just SingleObject
paramsAsMultipleObjects = preferParams == Just MultipleObjects
(argsCTE, args)
| null pgArgs = (ignoredBody, "")
| paramsAsSingleObject = ("pgrst_args AS (SELECT NULL)", "$1::json")
| otherwise = (
unwords [
normalizedBody <> ",",
"pgrst_args AS (",
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtArgs (\a -> " " <> pgaType a) <> ")",
")"]
, if paramsAsMultipleObjects
then fmtArgs (\a -> " := pgrst_args." <> pgFmtIdent (pgaName a))
else fmtArgs (\a -> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM pgrst_args LIMIT 1)")
)
fmtArgs :: (PgArg -> SqlFragment) -> SqlFragment
fmtArgs argFrag = intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> argFrag a) <$> pgArgs)
sourceBody :: SqlFragment
sourceBody
| paramsAsMultipleObjects =
if returnsScalar
then "SELECT " <> callIt <> " AS pgrst_scalar FROM pgrst_args"
else unwords [ "SELECT pgrst_lat_args.*"
, "FROM pgrst_args,"
, "LATERAL ( SELECT " <> returned_columns <> " FROM " <> callIt <> " ) pgrst_lat_args" ]
| otherwise =
if returnsScalar
then "SELECT " <> callIt <> " AS pgrst_scalar"
else "SELECT " <> returned_columns <> " FROM " <> callIt
callIt :: SqlFragment
callIt = fromQi qi <> "(" <> args <> ")"
returned_columns :: SqlFragment
returned_columns
| null returnings = "*"
| otherwise = intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName qi) <$> returnings)
-- | SQL query meant for COUNTing the root node of the Tree.
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.
-- SELECT 1 is done instead of SELECT * to prevent doing expensive operations(like functions based on the columns)
-- inside the FROM target.
readRequestToCountQuery :: ReadRequest -> SqlQuery
readRequestToCountQuery (Node (Select{from=qi, where_=logicForest}, _) _) =
unwords [
"SELECT 1",
"FROM " <> fromQi qi,
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest)) `emptyOnFalse` null logicForest
]
limitedQuery :: SqlQuery -> Maybe Integer -> SqlQuery
limitedQuery query maxRows = query <> maybe mempty (\x -> " LIMIT " <> show x) maxRows
setLocalQuery :: Text -> (Text, Text) -> SqlQuery
setLocalQuery prefix (k, v) =
"SET LOCAL " <> pgFmtIdent (prefix <> k) <> " = " <> pgFmtLit v <> ";"
setLocalSearchPathQuery :: [Text] -> SqlQuery
setLocalSearchPathQuery vals =
"SET LOCAL search_path = " <> intercalate ", " (pgFmtLit <$> vals) <> ";"
-85
View File
@@ -1,85 +0,0 @@
module PostgREST.QueryBuilder.Procedure where
import Data.Maybe
import Data.Text (intercalate, unwords)
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as H
import PostgREST.QueryBuilder.Private
import PostgREST.Types
import Protolude hiding (cast,
intercalate, replace)
import Text.InterpolatedString.Perl6 (qc)
type ProcResults = (Maybe Int64, Int64, ByteString, ByteString)
callProc :: QualifiedIdentifier -> [PgArg] -> Bool -> SqlQuery -> SqlQuery -> Bool ->
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion ->
H.Statement ByteString (Maybe ProcResults)
callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle paramsAsSingleObject asCsv asBinary binaryField pgVer =
unicodeStatement sql (param HE.unknown) decodeProc True
where
sql =[qc|
WITH
{argsRecord},
{sourceCTEName} AS (
{sourceBody}
)
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 [
normalizedBody <> ",",
"_args_record AS (",
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <>
intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " " <> pgaType a) <$> pgArgs) <> ")",
")"]
, intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " := _args_record." <> pgFmtIdent (pgaName a)) <$> pgArgs))
sourceBody :: SqlFragment
sourceBody
| paramsAsSingleObject || null pgArgs =
if returnsScalar
then [qc| SELECT {fromQi qi}({args}) |]
else [qc| SELECT * FROM {fromQi qi}({args}) |]
| otherwise =
if returnsScalar
then [qc| SELECT {fromQi qi}({args}) FROM _args_record |]
else [qc| SELECT _.*
FROM _args_record,
LATERAL ( SELECT * FROM {fromQi qi}({args}) ) _ |]
bodyF
| returnsScalar = scalarBodyF
| isSingle = asJsonSingleF
| asCsv = asCsvF
| isJust binaryField = asBinaryF $ fromJust binaryField
| otherwise = asJsonF
scalarBodyF
| asBinary = asBinaryF _procName
| otherwise = unwords [
"CASE",
"WHEN pg_catalog.count(_postgrest_t) = 1",
"THEN (json_agg(_postgrest_t." <> pgFmtIdent _procName <> ")->0)::character varying",
"ELSE (json_agg(_postgrest_t." <> pgFmtIdent _procName <> "))::character varying",
"END"]
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 = (,,,) <$> nullableColumn HD.int8 <*> column HD.int8
<*> column HD.bytea <*> column HD.bytea
@@ -1,32 +0,0 @@
module PostgREST.QueryBuilder.ReadStatement where
import Data.Maybe
import Data.Text (intercalate)
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as H
import PostgREST.QueryBuilder.Private
import PostgREST.Types
import Protolude hiding (cast,
intercalate, replace)
import Text.InterpolatedString.Perl6 (qc)
createReadStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool -> Maybe FieldName ->
H.Statement () ResultsWithCount
createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField =
unicodeStatement sql HE.noParams 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
@@ -1,53 +0,0 @@
module PostgREST.QueryBuilder.WriteStatement where
import Data.Maybe
import Data.Text (intercalate, unwords)
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as H
import PostgREST.ApiRequest (PreferRepresentation (..))
import PostgREST.QueryBuilder.Private
import PostgREST.Types
import Protolude hiding (cast,
intercalate, replace)
import Text.InterpolatedString.Perl6 (qc)
createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool ->
PreferRepresentation -> [Text] ->
H.Statement ByteString (Maybe ResultsWithCount)
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys =
unicodeStatement sql (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 isInsert
then unwords [
"CASE",
"WHEN pg_catalog.count(_postgrest_t) = 1 THEN",
"coalesce(" <> locationF pKeys <> ", " <> noLocationF <> ")",
"ELSE " <> noLocationF,
"END AS header"]
else noLocationF <> "AS header",
if rep == Full
then bodyF <> " AS body"
else "''"
]
bodyF
| asCsv = asCsvF
| wantSingle = asJsonSingleF
| otherwise = asJsonF
+33 -2
View File
@@ -1,6 +1,6 @@
{-| {-|
Module : PostgREST.RangeQuery Module : PostgREST.RangeQuery
Description : Logic regarding the `Range` header and `limit`, `offset` querystring arguments. Description : Logic regarding the `Range`/`Content-Range` headers and `limit`/`offset` querystring arguments.
-} -}
module PostgREST.RangeQuery ( module PostgREST.RangeQuery (
rangeParse rangeParse
@@ -11,6 +11,8 @@ module PostgREST.RangeQuery (
, rangeGeq , rangeGeq
, allRange , allRange
, NonnegRange , NonnegRange
, rangeStatusHeader
, contentRangeH
) where ) where
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
@@ -22,8 +24,10 @@ import Control.Applicative
import Data.Ranged.Boundaries import Data.Ranged.Boundaries
import Data.Ranged.Ranges import Data.Ranged.Ranges
import Network.HTTP.Types.Header import Network.HTTP.Types.Header
import Network.HTTP.Types.Status
import Protolude import Protolude hiding (toS)
import Protolude.Conv (toS)
type NonnegRange = Range Integer type NonnegRange = Range Integer
@@ -70,3 +74,30 @@ allRange = rangeGeq 0
rangeLeq :: Integer -> NonnegRange rangeLeq :: Integer -> NonnegRange
rangeLeq n = rangeLeq n =
Range BoundaryBelowAll (BoundaryAbove n) Range BoundaryBelowAll (BoundaryAbove n)
rangeStatusHeader :: NonnegRange -> Int64 -> Maybe Int64 -> (Status, Header)
rangeStatusHeader topLevelRange 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)
where
rangeStatus :: Integer -> Integer -> Maybe Integer -> Status
rangeStatus _ _ Nothing = status200
rangeStatus lower upper (Just total)
| lower > total = status416 -- 416 Range Not Satisfiable
| (1 + upper - lower) < total = status206 -- 206 Partial Content
| otherwise = status200 -- 200 OK
contentRangeH :: (Integral a, Show a) => a -> a -> Maybe a -> Header
contentRangeH lower upper total =
("Content-Range", toUtf8 headerValue)
where
headerValue = rangeString <> "/" <> totalString :: Text
rangeString
| totalNotZero && fromInRange = show lower <> "-" <> show upper
| otherwise = "*"
totalString = maybe "*" show total
totalNotZero = maybe True (0 /=) total
fromInRange = lower <= upper
+183
View File
@@ -0,0 +1,183 @@
{-|
Module : PostgREST.Statements
Description : PostgREST single SQL statements.
This module constructs single SQL statements that can be parametrized and prepared.
- It consumes the SqlQuery types generated by the QueryBuilder module.
- It generates the body format and some headers of the final HTTP response.
TODO: Currently, createReadStatement is not using prepared statements. See https://github.com/PostgREST/postgrest/issues/718.
-}
module PostgREST.Statements (
createWriteStatement
, createReadStatement
, callProcStatement
, createExplainStatement
) where
import Control.Lens ((^?))
import Data.Aeson as JSON
import qualified Data.Aeson.Lens as L
import qualified Data.ByteString.Char8 as BS
import Data.Maybe
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as H
import PostgREST.Private.Common
import PostgREST.Private.QueryFragment
import PostgREST.Types
import Protolude hiding (cast,
replace, toS)
import Protolude.Conv (toS)
import Text.InterpolatedString.Perl6 (qc)
{-| 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, Either Text [GucHeader])
createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool ->
PreferRepresentation -> [Text] -> PgVersion ->
H.Statement ByteString ResultsWithCount
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys pgVer =
unicodeStatement sql (param HE.unknown) decodeStandard True
where
sql = [qc|
WITH
{sourceCTEName} AS ({mutateQuery})
SELECT
'' AS total_result_set,
pg_catalog.count(_postgrest_t) AS page_total,
{locF} AS header,
{bodyF} AS body,
{responseHeadersF pgVer} AS response_headers
FROM ({selectF}) _postgrest_t |]
locF =
if isInsert && rep `elem` [Full, HeadersOnly]
then unwords [
"CASE WHEN pg_catalog.count(_postgrest_t) = 1",
"THEN coalesce(" <> locationF pKeys <> ", " <> noLocationF <> ")",
"ELSE " <> noLocationF,
"END"]
else noLocationF
bodyF
| rep `elem` [None, HeadersOnly] = "''"
| asCsv = asCsvF
| wantSingle = asJsonSingleF
| otherwise = asJsonF
selectF
-- prevent using any of the column names in ?select= when no response is returned from the CTE
| rep `elem` [None, HeadersOnly] = "SELECT * FROM " <> sourceCTEName
| otherwise = selectQuery
decodeStandard :: HD.Result ResultsWithCount
decodeStandard =
fromMaybe (Nothing, 0, [], mempty, Right []) <$> HD.rowMaybe standardRow
createReadStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion ->
H.Statement () ResultsWithCount
createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField pgVer =
unicodeStatement sql HE.noParams decodeStandard False
where
sql = [qc|
WITH
{sourceCTEName} AS ({selectQuery})
{countCTEF}
SELECT
{countResultF} AS total_result_set,
pg_catalog.count(_postgrest_t) AS page_total,
{noLocationF} AS header,
{bodyF} AS body,
{responseHeadersF pgVer} AS response_headers
FROM ( SELECT * FROM {sourceCTEName}) _postgrest_t |]
(countCTEF, countResultF) = countF countQuery countTotal
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| isJust binaryField = asBinaryF $ fromJust binaryField
| otherwise = asJsonF
decodeStandard :: HD.Result ResultsWithCount
decodeStandard =
HD.singleRow standardRow
{-| 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.
-}
standardRow :: HD.Row ResultsWithCount
standardRow = (,,,,) <$> nullableColumn HD.int8 <*> column HD.int8
<*> column header <*> column HD.bytea <*> column decodeGucHeaders
where
header = HD.array $ HD.dimension replicateM $ element HD.bytea
type ProcResults = (Maybe Int64, Int64, ByteString, Either Text [GucHeader])
callProcStatement :: Bool -> SqlQuery -> SqlQuery -> SqlQuery -> Bool ->
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion ->
H.Statement ByteString ProcResults
callProcStatement returnsScalar callProcQuery selectQuery countQuery countTotal isSingle asCsv asBinary multObjects binaryField pgVer =
unicodeStatement sql (param HE.unknown) decodeProc True
where
sql = [qc|
WITH {sourceCTEName} AS ({callProcQuery})
{countCTEF}
SELECT
{countResultF} AS total_result_set,
pg_catalog.count(_postgrest_t) AS page_total,
{bodyF} AS body,
{responseHeadersF pgVer} AS response_headers
FROM ({selectQuery}) _postgrest_t;|]
(countCTEF, countResultF) = countF countQuery countTotal
bodyF
| returnsScalar = scalarBodyF
| isSingle = asJsonSingleF
| asCsv = asCsvF
| isJust binaryField = asBinaryF $ fromJust binaryField
| otherwise = asJsonF
scalarBodyF
| asBinary = asBinaryF "pgrst_scalar"
| multObjects = "json_agg(_postgrest_t.pgrst_scalar)::character varying"
| otherwise = "(json_agg(_postgrest_t.pgrst_scalar)->0)::character varying"
decodeProc :: HD.Result ProcResults
decodeProc =
fromMaybe (Just 0, 0, mempty, Right []) <$> HD.rowMaybe procRow
where
procRow = (,,,) <$> nullableColumn HD.int8 <*> column HD.int8
<*> column HD.bytea <*> column decodeGucHeaders
createExplainStatement :: SqlQuery -> H.Statement () (Maybe Int64)
createExplainStatement countQuery =
unicodeStatement sql HE.noParams decodeExplain False
where
sql = [qc| EXPLAIN (FORMAT JSON) {countQuery} |]
-- |
-- An `EXPLAIN (FORMAT JSON) select * from items;` output looks like this:
-- [{
-- "Plan": {
-- "Node Type": "Seq Scan", "Parallel Aware": false, "Relation Name": "items",
-- "Alias": "items", "Startup Cost": 0.00, "Total Cost": 32.60,
-- "Plan Rows": 2260,"Plan Width": 8} }]
-- We only obtain the Plan Rows here.
decodeExplain :: HD.Result (Maybe Int64)
decodeExplain =
let row = HD.singleRow $ column HD.bytea in
(^? L.nth 0 . L.key "Plan" . L.key "Plan Rows" . L._Integral) <$> row
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Statement a b
unicodeStatement = H.Statement . encodeUtf8
decodeGucHeaders :: HD.Value (Either Text [GucHeader])
decodeGucHeaders = first toS . JSON.eitherDecode . toS <$> HD.bytea
+138 -52
View File
@@ -2,10 +2,14 @@
Module : PostgREST.Types Module : PostgREST.Types
Description : PostgREST common types and functions used by the rest of the modules Description : PostgREST common types and functions used by the rest of the modules
-} -}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
module PostgREST.Types where module PostgREST.Types where
import Control.Lens.Getter (view)
import Control.Lens.Tuple (_1)
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS (c2w) import qualified Data.ByteString.Internal as BS (c2w)
@@ -20,7 +24,8 @@ import Network.HTTP.Types.Header (Header, hContentType)
import Data.Tree import Data.Tree
import PostgREST.RangeQuery (NonnegRange) import PostgREST.RangeQuery (NonnegRange)
import Protolude import Protolude hiding (toS)
import Protolude.Conv (toS)
-- | Enumeration of currently supported response content types -- | Enumeration of currently supported response content types
data ContentType = CTApplicationJSON | CTSingularJSON data ContentType = CTApplicationJSON | CTSingularJSON
@@ -56,18 +61,53 @@ decodeContentType ct = case BS.takeWhile (/= BS.c2w ';') ct of
"*/*" -> CTAny "*/*" -> CTAny
ct' -> CTOther ct' ct' -> CTOther ct'
-- | A SQL query that can be executed independently
type SqlQuery = Text
-- | A part of a SQL query that cannot be executed independently
type SqlFragment = Text
data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq
instance Show PreferResolution where instance Show PreferResolution where
show MergeDuplicates = "resolution=merge-duplicates" show MergeDuplicates = "resolution=merge-duplicates"
show IgnoreDuplicates = "resolution=ignore-duplicates" show IgnoreDuplicates = "resolution=ignore-duplicates"
-- | How to return the mutated data. From https://tools.ietf.org/html/rfc7240#section-4.2
data PreferRepresentation = Full -- ^ Return the body plus the Location header(in case of POST).
| HeadersOnly -- ^ Return the Location header(in case of POST). This needs a SELECT privilege on the pk.
| None -- ^ Return nothing from the mutated data.
deriving Eq
instance Show PreferRepresentation where
show Full = "return=representation"
show None = "return=minimal"
show HeadersOnly = mempty
data PreferParameters
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure
| MultipleObjects -- ^ Pass an array of json objects as params to a stored procedure
deriving Eq
instance Show PreferParameters where
show SingleObject = "params=single-object"
show MultipleObjects = "params=multiple-objects"
data PreferCount
= ExactCount -- ^ exact count(slower)
| PlannedCount -- ^ PostgreSQL query planner rows count guess. Done by using EXPLAIN {query}.
| EstimatedCount -- ^ use the query planner rows if the count is superior to max-rows, otherwise get the exact count.
deriving Eq
instance Show PreferCount where
show ExactCount = "count=exact"
show PlannedCount = "count=planned"
show EstimatedCount = "count=estimated"
data DbStructure = DbStructure { data DbStructure = DbStructure {
dbTables :: [Table] dbTables :: [Table]
, dbColumns :: [Column] , dbColumns :: [Column]
, dbRelations :: [Relation] , dbRelations :: [Relation]
, dbPrimaryKeys :: [PrimaryKey] , dbPrimaryKeys :: [PrimaryKey]
-- ProcDescription is a list because a function can be overloaded , dbProcs :: ProcsMap
, dbProcs :: M.HashMap Text [ProcDescription]
, pgVersion :: PgVersion , pgVersion :: PgVersion
} deriving (Show, Eq) } deriving (Show, Eq)
@@ -93,7 +133,8 @@ data ProcVolatility = Volatile | Stable | Immutable
deriving (Eq, Show, Ord) deriving (Eq, Show, Ord)
data ProcDescription = ProcDescription { data ProcDescription = ProcDescription {
pdName :: Text pdSchema :: Schema
, pdName :: Text
, pdDescription :: Maybe Text , pdDescription :: Maybe Text
, pdArgs :: [PgArg] , pdArgs :: [PgArg]
, pdReturnType :: RetType , pdReturnType :: RetType
@@ -102,18 +143,23 @@ data ProcDescription = ProcDescription {
-- Order by least number of args in the case of overloaded functions -- Order by least number of args in the case of overloaded functions
instance Ord ProcDescription where instance Ord ProcDescription where
ProcDescription name1 des1 args1 rt1 vol1 `compare` ProcDescription name2 des2 args2 rt2 vol2 ProcDescription schema1 name1 des1 args1 rt1 vol1 `compare` ProcDescription schema2 name2 des2 args2 rt2 vol2
| name1 == name2 && length args1 < length args2 = LT | schema1 == schema2 && name1 == name2 && length args1 < length args2 = LT
| name1 == name2 && length args1 > length args2 = GT | schema2 == schema2 && name1 == name2 && length args1 > length args2 = GT
| otherwise = (name1, des1, args1, rt1, vol1) `compare` (name2, des2, args2, rt2, vol2) | otherwise = (schema1, name1, des1, args1, rt1, vol1) `compare` (schema2, name2, des2, args2, rt2, vol2)
-- | A map of all procs, all of which can be overloaded(one entry will have more than one ProcDescription).
-- | It uses a HashMap for a faster lookup.
type ProcsMap = M.HashMap QualifiedIdentifier [ProcDescription]
{-| {-|
Search a pg procedure by its parameters. Since a function can be overloaded, the name is not enough to find it. Search a pg procedure by its parameters. Since a function can be overloaded, the name is not enough to find it.
An overloaded function can have a different volatility or even a different return type. An overloaded function can have a different volatility or even a different return type.
Ideally, handling overloaded functions should be left to pg itself. But we need to know certain proc attributes in advance.
-} -}
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> M.HashMap Text [ProcDescription] -> Maybe ProcDescription findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> Maybe ProcDescription
findProc qi payloadKeys paramsAsSingleObject allProcs = findProc qi payloadKeys paramsAsSingleObject allProcs =
case M.lookup (qiName qi) allProcs of case M.lookup qi allProcs of
Nothing -> Nothing Nothing -> Nothing
Just [proc] -> Just proc -- if it's not an overloaded function then immediately get the ProcDescription Just [proc] -> Just proc -- if it's not an overloaded function then immediately get the ProcDescription
Just procs -> find matches procs -- Handle overloaded functions case Just procs -> find matches procs -- Handle overloaded functions case
@@ -135,10 +181,19 @@ specifiedProcArgs keys proc =
in in
(\k -> fromMaybe (PgArg k "text" True) (find ((==) k . pgaName) args)) <$> S.toList keys (\k -> fromMaybe (PgArg k "text" True) (find ((==) k . pgaName) args)) <$> S.toList keys
procReturnsScalar :: ProcDescription -> Bool
procReturnsScalar proc = case proc of
ProcDescription{pdReturnType = (Single (Scalar _))} -> True
_ -> False
procTableName :: ProcDescription -> Maybe TableName
procTableName proc = case pdReturnType proc of
SetOf (Composite qi) -> Just $ qiName qi
Single (Composite qi) -> Just $ qiName qi
_ -> Nothing
type Schema = Text type Schema = Text
type TableName = Text type TableName = Text
type SqlQuery = Text
type SqlFragment = Text
data Table = Table { data Table = Table {
tableSchema :: Schema tableSchema :: Schema
@@ -150,6 +205,9 @@ data Table = Table {
instance Eq Table where instance Eq Table where
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2 Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2
tableQi :: Table -> QualifiedIdentifier
tableQi Table{tableSchema=s, tableName=n} = QualifiedIdentifier s n
newtype ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord) newtype ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord)
data Column = data Column =
@@ -171,8 +229,8 @@ data Column =
instance Eq Column where instance Eq Column where
Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2 Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2
-- | A view column that refers to a table column -- | The source table column a view column refers to
type Synonym = (Column, ViewColumn) type SourceColumn = (Column, ViewColumn)
type ViewColumn = Column type ViewColumn = Column
data PrimaryKey = PrimaryKey { data PrimaryKey = PrimaryKey {
@@ -203,42 +261,56 @@ data OrderTerm = OrderTerm {
data QualifiedIdentifier = QualifiedIdentifier { data QualifiedIdentifier = QualifiedIdentifier {
qiSchema :: Schema qiSchema :: Schema
, qiName :: TableName , qiName :: TableName
} deriving (Show, Eq, Ord) } deriving (Show, Eq, Ord, Generic)
instance Hashable QualifiedIdentifier
-- | The relationship [cardinality](https://en.wikipedia.org/wiki/Cardinality_(data_modeling)).
-- | TODO: missing one-to-one
data Cardinality = O2M -- ^ one-to-many, previously known as Parent
| M2O -- ^ many-to-one, previously known as Child
| M2M -- ^ many-to-many, previously known as Many
deriving Eq
instance Show Cardinality where
show O2M = "o2m"
show M2O = "m2o"
show M2M = "m2m"
data RelationType = Child | Parent | Many | Root deriving (Show, Eq) type ConstraintName = Text
{-| {-|
The name 'Relation' here is used with the meaning "Relation"ship between two tables.
"What is the relation between the current node and the parent node". The order of the relColumns and relFColumns should be maintained to get the join conditions right.
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 TODO merge relColumns and relFColumns to a tuple or Data.Bimap
-} -}
data Relation = Relation { data Relation = Relation {
relTable :: Table relTable :: Table
, relColumns :: [Column] , relColumns :: [Column]
, relFTable :: Table , relConstraint :: Maybe ConstraintName -- ^ Just on O2M/M2O, Nothing on M2M
, relFColumns :: [Column] , relFTable :: Table
, relType :: RelationType , relFColumns :: [Column]
-- The Link attrs are used when RelationType == Many , relType :: Cardinality
, relLinkTable :: Maybe Table , relJunction :: Maybe Junction -- ^ Junction for M2M Cardinality
, relLinkCols1 :: Maybe [Column]
, relLinkCols2 :: Maybe [Column]
} deriving (Show, Eq) } deriving (Show, Eq)
isSelfJoin :: Relation -> Bool -- | Junction table on an M2M relationship
isSelfJoin r = relType r /= Root && relTable r == relFTable r data Junction = Junction {
junTable :: Table
, junConstraint1 :: Maybe ConstraintName
, junCols1 :: [Column]
, junConstraint2 :: Maybe ConstraintName
, junCols2 :: [Column]
} deriving (Show, Eq)
isSelfReference :: Relation -> Bool
isSelfReference r = relTable r == relFTable r
data PayloadJSON = data PayloadJSON =
-- | Cached attributes of a JSON payload -- | Cached attributes of a JSON payload
ProcessedJSON { ProcessedJSON {
-- | This is the raw ByteString that comes from the request body. -- | 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 -- 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 -- had high memory usage, see https://github.com/PostgREST/postgrest/pull/1005 for more details
pjRaw :: BL.ByteString 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 -- | 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 , pjKeys :: S.Set Text
}| }|
@@ -333,24 +405,33 @@ type RpcQParam = (Text, Text)
Custom guc header, it's obtained by parsing the json in a: Custom guc header, it's obtained by parsing the json in a:
`SET LOCAL "response.headers" = '[{"Set-Cookie": ".."}]' `SET LOCAL "response.headers" = '[{"Set-Cookie": ".."}]'
-} -}
newtype GucHeader = GucHeader (Text, Text) newtype GucHeader = GucHeader (CI.CI ByteString, ByteString)
deriving (Show, Eq)
instance JSON.FromJSON GucHeader where instance JSON.FromJSON GucHeader where
parseJSON (JSON.Object o) = case headMay (M.toList o) of parseJSON (JSON.Object o) = case headMay (M.toList o) of
Just (k, JSON.String s) | M.size o == 1 -> pure $ GucHeader (k, s) Just (k, JSON.String s) | M.size o == 1 -> pure $ GucHeader (CI.mk $ toS k, toS s)
| otherwise -> mzero | otherwise -> mzero
_ -> mzero _ -> mzero
parseJSON _ = mzero parseJSON _ = mzero
toHeaders :: [GucHeader] -> [Header] unwrapGucHeader :: GucHeader -> Header
toHeaders = map $ \(GucHeader (k, v)) -> (CI.mk $ toS k, toS v) unwrapGucHeader (GucHeader (k, v)) = (k, v)
-- | Add headers not already included to allow the user to override them instead of duplicating them
addHeadersIfNotIncluded :: [Header] -> [Header] -> [Header]
addHeadersIfNotIncluded newHeaders initialHeaders =
filter (\(nk, _) -> isNothing $ find (\(ik, _) -> ik == nk) initialHeaders) newHeaders ++
initialHeaders
{-| {-|
This type will hold information about which particular 'Relation' between two tables to choose when there are multiple ones. 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. 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 EmbedHint)
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe RelationDetail) -- | Disambiguates an embedding operation when there's multiple relationships between two tables.
-- | Can be the name of a foreign key constraint, column name or the junction in an m2m relationship.
type EmbedHint = Text
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"] -- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"]
type EmbedPath = [Text] type EmbedPath = [Text]
data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq) data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq)
@@ -359,11 +440,11 @@ data JoinCondition = JoinCondition (QualifiedIdentifier, FieldName)
data ReadQuery = Select { data ReadQuery = Select {
select :: [SelectItem] select :: [SelectItem]
, from :: TableName , from :: QualifiedIdentifier
-- | A table alias is used in case of self joins -- | A table alias is used in case of self joins
, fromAlias :: Maybe Alias , fromAlias :: Maybe Alias
-- | Only used for Many to Many joins. Parent and Child joins use explicit joins. -- | Only used for Many to Many joins. Parent and Child joins use explicit joins.
, implicitJoins :: [TableName] , implicitJoins :: [QualifiedIdentifier]
, where_ :: [LogicTree] , where_ :: [LogicTree]
, joinConditions :: [JoinCondition] , joinConditions :: [JoinCondition]
, order :: [OrderTerm] , order :: [OrderTerm]
@@ -372,31 +453,35 @@ data ReadQuery = Select {
data MutateQuery = data MutateQuery =
Insert { Insert {
in_ :: TableName in_ :: QualifiedIdentifier
, insCols :: S.Set FieldName , insCols :: S.Set FieldName
, onConflict :: Maybe (PreferResolution, [FieldName]) , onConflict :: Maybe (PreferResolution, [FieldName])
, where_ :: [LogicTree] , where_ :: [LogicTree]
, returning :: [FieldName] , returning :: [FieldName]
}| }|
Update { Update {
in_ :: TableName in_ :: QualifiedIdentifier
, updCols :: S.Set FieldName , updCols :: S.Set FieldName
, where_ :: [LogicTree] , where_ :: [LogicTree]
, returning :: [FieldName] , returning :: [FieldName]
}| }|
Delete { Delete {
in_ :: TableName in_ :: QualifiedIdentifier
, where_ :: [LogicTree] , where_ :: [LogicTree]
, returning :: [FieldName] , returning :: [FieldName]
} deriving (Show, Eq) } deriving (Show, Eq)
data DbRequest = DbRead ReadRequest | DbMutate MutateRequest
type ReadRequest = Tree ReadNode type ReadRequest = Tree ReadNode
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Depth))
-- Depth of the ReadRequest tree
type Depth = Integer
type MutateRequest = MutateQuery type MutateRequest = MutateQuery
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe EmbedHint, Depth))
type Depth = Integer
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
fstFieldNames :: ReadRequest -> [FieldName]
fstFieldNames (Node (sel, _) _) =
fst . view _1 <$> select sel
data PgVersion = PgVersion { data PgVersion = PgVersion {
pgvNum :: Int32 pgvNum :: Int32
, pgvName :: Text , pgvName :: Text
@@ -433,16 +518,17 @@ pgVersion112 = PgVersion 110002 "11.2"
pgVersion114 :: PgVersion pgVersion114 :: PgVersion
pgVersion114 = PgVersion 110004 "11.4" pgVersion114 = PgVersion 110004 "11.4"
pgVersion121 :: PgVersion
pgVersion121 = PgVersion 120001 "12.1"
sourceCTEName :: SqlFragment sourceCTEName :: SqlFragment
sourceCTEName = "pg_source" sourceCTEName = "pgrst_source"
-- | full jspath, e.g. .property[0].attr.detail -- | full jspath, e.g. .property[0].attr.detail
type JSPath = [JSPathExp] type JSPath = [JSPathExp]
-- | jspath expression, e.g. .property, .property[0] or ."property-dash" -- | jspath expression, e.g. .property, .property[0] or ."property-dash"
data JSPathExp = JSPKey Text | JSPIdx Int deriving (Eq, Show) data JSPathExp = JSPKey Text | JSPIdx Int deriving (Eq, Show)
-- | Current database connection status data ConnectionStatus -- | Current database connection status data ConnectionStatus
data ConnectionStatus data ConnectionStatus
= NotConnected = NotConnected
+9 -19
View File
@@ -1,21 +1,11 @@
# stack.yaml is used for circle-ci tests. Profiling build fails on circleci # stack is used for circleci and appveyor CI builds
# with GHC 8.6, so we build with 8.4 for now. resolver: lts-15.8
resolver: lts-13.29
extra-deps:
- Ranged-sets-0.4.0
- configurator-pg-0.1.0.3
- http-types-0.12.3
- hasql-1.4
- hasql-pool-0.5.1
- hasql-transaction-0.7.2
- text-builder-0.6.5.1
- deferred-folds-0.9.10.1
- primitive-0.6.4.0
- jose-0.8.1.0
- text-printer-0.5.0.1
- network-2.7.0.1
ghc-options:
postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints -optP-Wno-nonportable-include-path
nix: nix:
packages: [pcre, pkgconfig, postgresql, zlib] packages: [pcre, pkgconfig, postgresql, zlib]
# disable pure by default so that the test enviroment can be passed
pure: false
extra-deps:
- configurator-pg-0.2.2@sha256:8f3f7c66dd4a53852283c70806ebc101a7219f8b01c3e80c5130e9749c998dc8,2987
- hspec-wai-json-0.10.1@sha256:67b405c38f0a9e2771480c8d3ecd8aeb8d8776a35d3b2906cb1b76c9538617e4,1629
- interpolatedstring-perl6-1.0.2@sha256:7ce49c8a69a2a1b89c001ed79db2aab656ffd0faf2a7a701a553b6deb5c8ba7f,1073
- protolude-0.3.0@sha256:8361b811b420585b122a7ba715aa5923834db6e8c36309bf267df2dbf66b95ef,2693
+20 -76
View File
@@ -5,92 +5,36 @@
packages: packages:
- completed: - completed:
hackage: Ranged-sets-0.4.0@sha256:04bb4ce482fbdc052c9ee3346ba210986b33002b8c3440b714d62750144f86b6,1373 hackage: configurator-pg-0.2.2@sha256:8f3f7c66dd4a53852283c70806ebc101a7219f8b01c3e80c5130e9749c998dc8,2987
pantry-tree: pantry-tree:
size: 566 size: 2462
sha256: ae6809a20be4da39729ac3c6e10b5c311b80628c767184da95903ecff6efc095 sha256: a669366632e19f396dfe52599d54414b9c92d5664ea4391c2c797be271f9aa0e
original: original:
hackage: Ranged-sets-0.4.0 hackage: configurator-pg-0.2.2@sha256:8f3f7c66dd4a53852283c70806ebc101a7219f8b01c3e80c5130e9749c998dc8,2987
- completed: - completed:
hackage: configurator-pg-0.1.0.3@sha256:ddccf34fef0a5c4f1364ec4c6fda459f089a592e18662b0263ed85ea943c9c70,2885 hackage: hspec-wai-json-0.10.1@sha256:67b405c38f0a9e2771480c8d3ecd8aeb8d8776a35d3b2906cb1b76c9538617e4,1629
pantry-tree: pantry-tree:
size: 1008 size: 349
sha256: 5eb16e5536e8bfd921286fb4c97eaaf0c1cb9bdbdf9630a7ffdf1cc64942b79b sha256: fb9e89b79cde3276baa484c860c6b9eeebdbc1a5c43301293351a25bc4c08e87
original: original:
hackage: configurator-pg-0.1.0.3 hackage: hspec-wai-json-0.10.1@sha256:67b405c38f0a9e2771480c8d3ecd8aeb8d8776a35d3b2906cb1b76c9538617e4,1629
- completed: - completed:
hackage: http-types-0.12.3@sha256:f35229edb1bc7b3ae27f961b2407dadb5bfa69d43a8f5337ab46cdc79ca4afe9,2035 hackage: interpolatedstring-perl6-1.0.2@sha256:7ce49c8a69a2a1b89c001ed79db2aab656ffd0faf2a7a701a553b6deb5c8ba7f,1073
pantry-tree: pantry-tree:
size: 833 size: 300
sha256: c9b77e1ba204fffbe4e1be80412bc48e47440a07e4b7db4cdc77d573a3e21b9a sha256: f7f0b06c58db814d1f12ed6f4f4cb67ff8c72e51da61c04db12c2aedb896542e
original: original:
hackage: http-types-0.12.3 hackage: interpolatedstring-perl6-1.0.2@sha256:7ce49c8a69a2a1b89c001ed79db2aab656ffd0faf2a7a701a553b6deb5c8ba7f,1073
- completed: - completed:
hackage: hasql-1.4@sha256:fcb1b0046c1e888b6c4cad53c972d23318dbc6ababa9ceb7d9cfd3b546732bcd,6515 hackage: protolude-0.3.0@sha256:8361b811b420585b122a7ba715aa5923834db6e8c36309bf267df2dbf66b95ef,2693
pantry-tree: pantry-tree:
size: 2567 size: 1644
sha256: 2e253e7f3052ae4f838d26191355eb0c0b9f344e265b7dec50e13b799ef32450 sha256: babf32b414f25f790b7a4ce6bae5c960bc51a11a289e7c47335b222e6762560c
original: original:
hackage: hasql-1.4 hackage: protolude-0.3.0
- completed:
hackage: hasql-pool-0.5.1@sha256:a98f2fc38f60eb037a8ac6c5e17591b090089e305f367219c4879812592aaafe,2436
pantry-tree:
size: 412
sha256: 22e4cea8c23ea0eaa871388236c1d3e10349d993fd1bbc602f40aeb4909d6100
original:
hackage: hasql-pool-0.5.1
- completed:
hackage: hasql-transaction-0.7.2@sha256:d6d8ceb0b32be75686fe31c4b5bc15c569a71023fc60394893508ea733e8714b,2835
pantry-tree:
size: 1028
sha256: 9bd8c7bf3e30d033192ff53c97e5f1f5e9c08ebfe9a1b3364ab0f9e8f2c2872e
original:
hackage: hasql-transaction-0.7.2
- completed:
hackage: text-builder-0.6.5.1@sha256:547f292707c7488c0fbee415adb5fa107d725b720f8697e966a3e3e00cac02cd,4210
pantry-tree:
size: 542
sha256: 5b2be8c9530d3460cadfe142429e0f82bb4b5c238737de473b6f9a25a33e232f
original:
hackage: text-builder-0.6.5.1
- completed:
hackage: deferred-folds-0.9.10.1@sha256:eb2634488e2a836da7d5aed9afd15ad2bead38817249f60bb68e459cc32fb0b0,2928
pantry-tree:
size: 958
sha256: b9132db4ffe78f11254871ed88071c5abe1ff41eeffcbec30ce484f24db81c19
original:
hackage: deferred-folds-0.9.10.1
- completed:
hackage: primitive-0.6.4.0@sha256:5b6a2c3cc70a35aabd4565fcb9bb1dd78fe2814a36e62428a9a1aae8c32441a1,2079
pantry-tree:
size: 1517
sha256: 5d5e591311664886e88ade3da6880c32adf0d1fe80c55f40a3c93bb91df8fdeb
original:
hackage: primitive-0.6.4.0
- completed:
hackage: jose-0.8.1.0@sha256:904e64203f0e074c4601529be2b57c94eec9fd588b19e16165391f8f9e84a6a0,3353
pantry-tree:
size: 1935
sha256: 8d5f80f184b61e89fedbf61d4d7b54ce85326fe67d8ec888117387522c06504d
original:
hackage: jose-0.8.1.0
- completed:
hackage: text-printer-0.5.0.1@sha256:9171204826a67c97bc3578968d8b3dcfbd81be08dfd18fef2af71eaf7b2737b1,1502
pantry-tree:
size: 461
sha256: 656053744f42551bc9cffd76746bfae77a249f54ca69497f6835e35312c7b5bb
original:
hackage: text-printer-0.5.0.1
- completed:
hackage: network-2.7.0.1@sha256:e8ab30822597c44f0520875699e005c8b284d19000bdacfddbc980c7dfa00bec,2823
pantry-tree:
size: 2313
sha256: 5729e7f6993505243e13fde01833e768024303052becb72f915d8cff1c20e177
original:
hackage: network-2.7.0.1
snapshots: snapshots:
- completed: - completed:
size: 500539 size: 492015
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/13/29.yaml url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/15/8.yaml
sha256: 006398c5e92d1d64737b7e98ae4d63987c36808814504d1451f56ebd98093f75 sha256: 926bc3d70249dd0ba05277ff00943c0addb35b627cb641752669e7cf771310d0
original: lts-13.29 original: lts-15.8
+1 -1
View File
@@ -4,7 +4,7 @@ ENV PATH /root/.local/bin:$PATH
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y wget libpq-dev pkg-config libpcre3 libpcre3-dev \ && apt-get install -y wget libpq-dev pkg-config libpcre3 libpcre3-dev \
postgresql-client debconf locales \ postgresql-client debconf locales build-essential libffi-dev libgmp-dev git \
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen \ && echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen \
&& locale-gen \ && locale-gen \
+1 -1
View File
@@ -11,7 +11,7 @@ import PostgREST.Types (PgVersion, pgVersion112)
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: PgVersion -> SpecWith Application spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion = spec actualPgVersion =
describe "and/or params used for complex boolean logic" $ do describe "and/or params used for complex boolean logic" $ do
context "used with GET" $ do context "used with GET" $ do
+1 -1
View File
@@ -11,7 +11,7 @@ import Protolude
import SpecHelper import SpecHelper
-- }}} -- }}}
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = describe "server started with asymmetric JWK" $ spec = describe "server started with asymmetric JWK" $
-- this test will stop working 9999999999s after the UNIX EPOCH -- this test will stop working 9999999999s after the UNIX EPOCH
+1 -1
View File
@@ -11,7 +11,7 @@ import Protolude hiding (get)
import SpecHelper import SpecHelper
-- }}} -- }}}
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = describe "test handling of aud claims in JWT" $ do spec = describe "test handling of aud claims in JWT" $ do
-- this test will stop working 9999999999s after the UNIX EPOCH -- this test will stop working 9999999999s after the UNIX EPOCH
+1 -1
View File
@@ -12,7 +12,7 @@ import PostgREST.Types (PgVersion, pgVersion112)
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: PgVersion -> SpecWith Application spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion = describe "authorization" $ do spec actualPgVersion = describe "authorization" $ do
let single = ("Accept","application/vnd.pgrst.object+json") let single = ("Accept","application/vnd.pgrst.object+json")
+1 -1
View File
@@ -11,7 +11,7 @@ import Protolude
import SpecHelper import SpecHelper
-- }}} -- }}}
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = describe "server started with binary JWT secret" $ spec = describe "server started with binary JWT secret" $
-- this test will stop working 9999999999s after the UNIX EPOCH -- this test will stop working 9999999999s after the UNIX EPOCH
+6 -7
View File
@@ -5,7 +5,6 @@
module Feature.ConcurrentSpec where module Feature.ConcurrentSpec where
import Control.Concurrent.Async (mapConcurrently) import Control.Concurrent.Async (mapConcurrently)
import Control.Monad (void)
import Network.Wai (Application) import Network.Wai (Application)
import Control.Monad.Base import Control.Monad.Base
@@ -19,9 +18,9 @@ import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "Queryiny in parallel" $ describe "Querying in parallel" $
it "should not raise 'transaction in progress' error" $ it "should not raise 'transaction in progress' error" $
raceTest 10 $ raceTest 10 $
get "/fakefake" get "/fakefake"
@@ -35,13 +34,13 @@ spec =
, matchHeaders = [] , matchHeaders = []
} }
raceTest :: Int -> WaiExpectation -> WaiExpectation raceTest :: Int -> WaiExpectation st -> WaiExpectation st
raceTest times = liftBaseDiscard go raceTest times = liftBaseDiscard go
where where
go test = void $ mapConcurrently (const test) [1..times] go test = void $ mapConcurrently (const test) [1..times]
instance MonadBaseControl IO WaiSession where instance MonadBaseControl IO (WaiSession st) where
type StM WaiSession a = StM Session a type StM (WaiSession st) a = StM Session a
liftBaseWith f = WaiSession $ liftBaseWith f = WaiSession $
liftBaseWith $ \runInBase -> liftBaseWith $ \runInBase ->
f $ \k -> runInBase (unWaiSession k) f $ \k -> runInBase (unWaiSession k)
@@ -49,5 +48,5 @@ instance MonadBaseControl IO WaiSession where
{-# INLINE liftBaseWith #-} {-# INLINE liftBaseWith #-}
{-# INLINE restoreM #-} {-# INLINE restoreM #-}
instance MonadBase IO WaiSession where instance MonadBase IO (WaiSession st) where
liftBase = liftIO liftBase = liftIO
+1 -1
View File
@@ -14,7 +14,7 @@ import Protolude
import SpecHelper import SpecHelper
-- }}} -- }}}
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "CORS" $ do describe "CORS" $ do
let preflightHeaders = [ let preflightHeaders = [
+39 -2
View File
@@ -5,11 +5,12 @@ import Network.Wai (Application)
import Network.HTTP.Types import Network.HTTP.Types
import Test.Hspec import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Text.Heredoc import Text.Heredoc
import Protolude hiding (get) import Protolude hiding (get)
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "Deleting" $ do describe "Deleting" $ do
context "existing record" $ do context "existing record" $ do
@@ -26,17 +27,32 @@ spec =
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/1"] , matchHeaders = ["Content-Range" <:> "*/1"]
} }
it "ignores ?select= when return not set or return=minimal" $ do
request methodDelete "/items?id=eq.3&select=id" [] ""
`shouldRespondWith` ""
{ matchStatus = 204
, matchHeaders = ["Content-Range" <:> "*/*"]
}
request methodDelete "/items?id=eq.3&select=id" [("Prefer", "return=minimal")] ""
`shouldRespondWith` ""
{ matchStatus = 204
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "returns the deleted item and shapes the response" $ it "returns the deleted item and shapes the response" $
request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] "" request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""
`shouldRespondWith` [str|[{"id":2,"name":"Two"}]|] `shouldRespondWith` [str|[{"id":2,"name":"Two"}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
it "can rename and cast the selected columns" $ it "can rename and cast the selected columns" $
request methodDelete "/complex_items?id=eq.3&select=ciId:id::text,ciName:name" [("Prefer", "return=representation")] "" request methodDelete "/complex_items?id=eq.3&select=ciId:id::text,ciName:name" [("Prefer", "return=representation")] ""
`shouldRespondWith` [str|[{"ciId":"3","ciName":"Three"}]|] `shouldRespondWith` [str|[{"ciId":"3","ciName":"Three"}]|]
it "can embed (parent) entities" $ it "can embed (parent) entities" $
request methodDelete "/tasks?id=eq.8&select=id,name,project(id)" [("Prefer", "return=representation")] "" request methodDelete "/tasks?id=eq.8&select=id,name,project:projects(id)" [("Prefer", "return=representation")] ""
`shouldRespondWith` [str|[{"id":8,"name":"Code OSX","project":{"id":4}}]|] `shouldRespondWith` [str|[{"id":8,"name":"Code OSX","project":{"id":4}}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
@@ -62,3 +78,24 @@ spec =
context "totally unknown route" $ context "totally unknown route" $
it "fails with 404" $ it "fails with 404" $
request methodDelete "/foozle?id=eq.101" [] "" `shouldRespondWith` 404 request methodDelete "/foozle?id=eq.101" [] "" `shouldRespondWith` 404
context "table with limited privileges" $ do
it "fails deleting the row when return=representation and selecting all the columns" $
request methodDelete "/app_users?id=eq.1" [("Prefer", "return=representation")] mempty
`shouldRespondWith` 401
it "succeeds deleting the row when return=representation and selecting only the privileged columns" $
request methodDelete "/app_users?id=eq.1&select=id,email" [("Prefer", "return=representation")]
[json| { "password": "passxyz" } |]
`shouldRespondWith` [json|[ { "id": 1, "email": "test@123.com" } ]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "suceeds deleting the row with no explicit select when using return=minimal" $
request methodDelete "/app_users?id=eq.2" [("Prefer", "return=minimal")] mempty
`shouldRespondWith` 204
it "suceeds deleting the row with no explicit select by default" $
request methodDelete "/app_users?id=eq.3" [] mempty
`shouldRespondWith` 204
+434
View File
@@ -0,0 +1,434 @@
module Feature.EmbedDisambiguationSpec where
import Network.Wai (Application)
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Text.Heredoc
import Protolude hiding (get)
import SpecHelper
spec :: SpecWith ((), Application)
spec =
describe "resource embedding disambiguation" $ do
context "ambiguous requests that give 300 Multiple Choices" $ do
it "errs when there's a table and view that point to the same fk" $
get "/message?select=id,body,sender(name,sent)" `shouldRespondWith`
[json|
{
"details": [
{
"cardinality": "m2o",
"relationship": "message_sender_fkey[sender][id]",
"origin": "test.message",
"target": "test.person"
},
{
"cardinality": "m2o",
"relationship": "message_sender_fkey[sender][id]",
"origin": "test.message",
"target": "test.person_detail"
}
],
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
"message": "More than one relationship was found for message and sender"
}
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
it "errs when there are o2m and m2m cardinalities to the target table" $
get "/sites?select=*,big_projects(*)" `shouldRespondWith`
[json|
{
"details": [
{
"cardinality": "m2o",
"relationship": "main_project[main_project_id][big_project_id]",
"origin": "test.sites",
"target": "test.big_projects"
},
{
"cardinality": "m2m",
"relationship": "test.jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",
"origin": "test.sites",
"target": "test.big_projects"
},
{
"cardinality": "m2m",
"relationship": "test.main_jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",
"origin": "test.sites",
"target": "test.big_projects"
}
],
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
"message": "More than one relationship was found for sites and big_projects"
}
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
it "errs on an ambiguous embed that has a circular reference" $
get "/agents?select=*,departments(*)" `shouldRespondWith`
[json|
{
"details": [
{
"cardinality": "m2o",
"relationship": "agents_department_id_fkey[department_id][id]",
"origin": "test.agents",
"target": "test.departments"
},
{
"cardinality": "o2m",
"relationship": "departments_head_id_fkey[id][head_id]",
"origin": "test.agents",
"target": "test.departments"
}
],
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
"message": "More than one relationship was found for agents and departments"
}
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
it "errs when there are more than two fks on a junction table(currently impossible to disambiguate, only choice is to split the table)" $
-- We have 4 possibilities for doing the junction JOIN here.
-- This could be solved by specifying two additional fks, like whatev_projects!fk1!fk2(*)
-- If the need arises this capability can be added later without causing a breaking change
get "/whatev_sites?select=*,whatev_projects(*)" `shouldRespondWith`
[json|
{
"details": [
{
"cardinality": "m2m",
"relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_1_fkey]",
"origin": "test.whatev_sites",
"target": "test.whatev_projects"
},
{
"cardinality": "m2m",
"relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_2_fkey]",
"origin": "test.whatev_sites",
"target": "test.whatev_projects"
},
{
"cardinality": "m2m",
"relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_1_fkey]",
"origin": "test.whatev_sites",
"target": "test.whatev_projects"
},
{
"cardinality": "m2m",
"relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_2_fkey]",
"origin": "test.whatev_sites",
"target": "test.whatev_projects"
}
],
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
"message": "More than one relationship was found for whatev_sites and whatev_projects"
}
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
context "disambiguating requests with embed hints" $ do
context "using FK to specify the relationship" $ do
it "can embed by FK name" $
get "/projects?id=in.(1,3)&select=id,name,client(id,name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed by FK name and select the FK column at the same time" $
get "/projects?id=in.(1,3)&select=id,name,client_id,client(id,name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed parent with view!fk and grandparent by using fk" $
get "/tasks?id=eq.1&select=id,name,projects_view!project(id,name,client(id,name))" `shouldRespondWith`
[str|[{"id":1,"name":"Design w7","projects_view":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]
it "can embed by using a composite FK name" $
get "/unit_workdays?select=unit_id,day,fst_shift(car_id,schedule(name)),snd_shift(camera_id,schedule(name))" `shouldRespondWith`
[json| [
{
"day": "2019-12-02",
"fst_shift": {
"car_id": "CAR-349",
"schedule": {
"name": "morning"
}
},
"snd_shift": {
"camera_id": "CAM-123",
"schedule": {
"name": "night"
}
},
"unit_id": 1
}
] |]
{ matchHeaders = [matchContentTypeJson] }
it "embeds by using two fks pointing to the same table" $
get "/orders?id=eq.1&select=id, name, billing(address), shipping(address)" `shouldRespondWith`
[json|[{"id":1,"name":"order 1","billing":{"address": "address 1"},"shipping":{"address": "address 2"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "fails if the fk is not known" $
get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`
[json|{"message":"Could not find foreign keys between these entities. No relationship found between message and person"}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson] }
it "can request a parent with fk" $
get "/comments?select=content,user(name)" `shouldRespondWith`
[json|[ { "content": "Needs to be delivered ASAP", "user": { "name": "Angela Martin" } } ]|]
{ matchHeaders = [matchContentTypeJson] }
it "can request two parents with fks" $
get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith`
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can specify a view!fk" $
get "/message?select=id,body,sender:person_detail!message_sender_fkey(name,sent),recipient:person_detail!message_recipient_fkey(name,received)&id=lt.4" `shouldRespondWith`
[json|
[{"id":1,"body":"Hello Jane","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}},
{"id":2,"body":"Hi John","sender":{"name":"Jane","sent":1},"recipient":{"name":"John","received":1}},
{"id":3,"body":"How are you doing?","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can specify a table!fk hint and request children 2 levels" $
get "/clients?id=eq.1&select=id,projects:projects!client(id,tasks(id))" `shouldRespondWith`
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can disambiguate with the fk in case of an o2m and m2m relationship to the same table" $
get "/sites?select=name,main_project(name)&site_id=eq.1" `shouldRespondWith`
[json| [ { "name": "site 1", "main_project": { "name": "big project 1" } } ] |]
{ matchHeaders = [matchContentTypeJson] }
context "using the column name of the FK to specify the relationship" $ do
it "can embed by column" $
get "/projects?id=in.(1,3)&select=id,name,client_id(id,name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed by column and select the column at the same time, if aliased" $
get "/projects?id=in.(1,3)&select=id,name,client_id,client:client_id(id,name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed parent by using view!column and grandparent by using the column" $
get "/tasks?id=eq.1&select=id,name,project:projects_view!project_id(id,name,client:client_id(id,name))" `shouldRespondWith`
[str|[{"id":1,"name":"Design w7","project":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]
it "can specify table!column" $
get "/message?select=id,body,sender:person!sender(name),recipient:person!recipient(name)&id=lt.4" `shouldRespondWith`
[json|
[{"id":1,"body":"Hello Jane","sender":{"name":"John"},"recipient":{"name":"Jane"}},
{"id":2,"body":"Hi John","sender":{"name":"Jane"},"recipient":{"name":"John"}},
{"id":3,"body":"How are you doing?","sender":{"name":"John"},"recipient":{"name":"Jane"}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "will embed using a column that has uppercase chars" $
get "/ghostBusters?select=escapeId(*)" `shouldRespondWith`
[json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "embeds by using two columns pointing to the same table" $
get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping_address_id(id)" `shouldRespondWith`
[json|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping_address_id":{"id":2}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can disambiguate with the column in case of an o2m and m2m relationship to the same table" $
get "/sites?select=name,main_project_id(name)&site_id=eq.1" `shouldRespondWith`
[json| [ { "name": "site 1", "main_project_id": { "name": "big project 1" } } ] |]
{ matchHeaders = [matchContentTypeJson] }
context "using the junction to disambiguate the request" $
it "can specify the junction of an m2m relationship" $ do
get "/sites?select=*,big_projects!jobs(name)&site_id=in.(1,2)" `shouldRespondWith`
[json|
[
{
"big_projects": [
{
"name": "big project 1"
}
],
"main_project_id": 1,
"name": "site 1",
"site_id": 1
},
{
"big_projects": [
{
"name": "big project 1"
},
{
"name": "big project 2"
}
],
"main_project_id": null,
"name": "site 2",
"site_id": 2
}
]
|]
get "/sites?select=*,big_projects!main_jobs(name)&site_id=in.(1,2)" `shouldRespondWith`
[json|
[
{
"big_projects": [
{
"name": "big project 1"
}
],
"main_project_id": 1,
"name": "site 1",
"site_id": 1
},
{
"big_projects": [],
"main_project_id": null,
"name": "site 2",
"site_id": 2
}
]
|]
{ matchHeaders = [matchContentTypeJson] }
context "using a FK column and a FK to specify the relationship" $
it "embeds by using a column and a fk pointing to the same table" $
get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping(id)" `shouldRespondWith`
[json|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping":{"id":2}}]|]
{ matchHeaders = [matchContentTypeJson] }
context "tables with self reference foreign keys" $ do
context "one self reference foreign key" $ do
it "embeds parents recursively" $
get "/family_tree?id=in.(3,4)&select=id,parent(id,name,parent(*))" `shouldRespondWith`
[json|[
{ "id": "3", "parent": { "id": "1", "name": "Parental Unit", "parent": null } },
{ "id": "4", "parent": { "id": "2", "name": "Kid One", "parent": { "id": "1", "name": "Parental Unit", "parent": null } } }
]|]
{ matchHeaders = [matchContentTypeJson] }
it "embeds children recursively" $
get "/family_tree?id=eq.1&select=id,name, children:family_tree!parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`
[json|[{
"id": "1", "name": "Parental Unit", "children": [
{ "id": "2", "name": "Kid One", "children": [ { "id": "4", "name": "Grandkid One" } ] },
{ "id": "3", "name": "Kid Two", "children": [ { "id": "5", "name": "Grandkid Two" } ] }
]
}]|] { matchHeaders = [matchContentTypeJson] }
it "embeds parent and then embeds children" $
get "/family_tree?id=eq.2&select=id,name,parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`
[json|[{
"id": "2", "name": "Kid One", "parent": {
"id": "1", "name": "Parental Unit", "children": [ { "id": "2", "name": "Kid One" }, { "id": "3", "name": "Kid Two"} ]
}
}]|] { matchHeaders = [matchContentTypeJson] }
context "two self reference foreign keys" $ do
it "embeds parents" $
get "/organizations?select=id,name,referee(id,name),auditor(id,name)&id=eq.3" `shouldRespondWith`
[json|[{
"id": 3, "name": "Acme",
"referee": {
"id": 1,
"name": "Referee Org"
},
"auditor": {
"id": 2,
"name": "Auditor Org"
}
}]|] { matchHeaders = [matchContentTypeJson] }
it "embeds children" $ do
get "/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1" `shouldRespondWith`
[json|[{
"id": 1, "name": "Referee Org",
"refereeds": [
{
"id": 3,
"name": "Acme"
},
{
"id": 4,
"name": "Umbrella"
}
]
}]|] { matchHeaders = [matchContentTypeJson] }
get "/organizations?select=id,name,auditees:organizations!auditor(id,name)&id=eq.2" `shouldRespondWith`
[json|[{
"id": 2, "name": "Auditor Org",
"auditees": [
{
"id": 3,
"name": "Acme"
},
{
"id": 4,
"name": "Umbrella"
}
]
}]|] { matchHeaders = [matchContentTypeJson] }
it "embeds other relations(manager) besides the self reference" $ do
get "/organizations?select=name,manager(name),referee(name,manager(name),auditor(name,manager(name))),auditor(name,manager(name),referee(name,manager(name)))&id=eq.5" `shouldRespondWith`
[json|[{
"name":"Cyberdyne",
"manager":{"name":"Cyberdyne Manager"},
"referee":{
"name":"Acme",
"manager":{"name":"Acme Manager"},
"auditor":{
"name":"Auditor Org",
"manager":{"name":"Auditor Manager"}}},
"auditor":{
"name":"Umbrella",
"manager":{"name":"Umbrella Manager"},
"referee":{
"name":"Referee Org",
"manager":{"name":"Referee Manager"}}}
}]|] { matchHeaders = [matchContentTypeJson] }
get "/organizations?select=name,manager(name),auditees:organizations!auditor(name,manager(name),refereeds:organizations!referee(name,manager(name)))&id=eq.2" `shouldRespondWith`
[json|[{
"name":"Auditor Org",
"manager":{"name":"Auditor Manager"},
"auditees":[
{"name":"Acme",
"manager":{"name":"Acme Manager"},
"refereeds":[
{"name":"Cyberdyne",
"manager":{"name":"Cyberdyne Manager"}},
{"name":"Oscorp",
"manager":{"name":"Oscorp Manager"}}]},
{"name":"Umbrella",
"manager":{"name":"Umbrella Manager"},
"refereeds":[]}]
}]|] { matchHeaders = [matchContentTypeJson] }
-- TODO Remove in next major version
describe "old dot '.' symbol, deprecated" $
it "still works" $ do
get "/clients?id=eq.1&select=id,projects:projects.client_id(id,tasks(id))" `shouldRespondWith`
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/tasks?select=id,users:users.users_tasks(id)" `shouldRespondWith`
[json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]
{ matchHeaders = [matchContentTypeJson] }
+1 -1
View File
@@ -9,7 +9,7 @@ import Test.Hspec.Wai.JSON
import Protolude import Protolude
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = describe "extra search path" $ do spec = describe "extra search path" $ do
it "finds the ltree <@ operator on the public schema" $ it "finds the ltree <@ operator on the public schema" $
+1 -1
View File
@@ -10,7 +10,7 @@ import Text.Heredoc
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper (acceptHdrs) import SpecHelper (acceptHdrs)
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = describe "When raw-media-types is set to \"text/html\"" $ spec = describe "When raw-media-types is set to \"text/html\"" $
it "can get raw output with Accept: text/html" $ it "can get raw output with Accept: text/html" $
request methodGet "/rpc/welcome.html" (acceptHdrs "text/html") "" request methodGet "/rpc/welcome.html" (acceptHdrs "text/html") ""
+288 -69
View File
@@ -2,7 +2,6 @@ module Feature.InsertSpec where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import Control.Monad (replicateM_, void)
import Data.List (lookup) import Data.List (lookup)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Network.Wai (Application) import Network.Wai (Application)
@@ -20,7 +19,7 @@ import PostgREST.Types (PgVersion, pgVersion112)
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: PgVersion -> SpecWith Application spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion = do spec actualPgVersion = do
describe "Posting new record" $ do describe "Posting new record" $ do
context "disparate json types" $ do context "disparate json types" $ do
@@ -48,6 +47,22 @@ spec actualPgVersion = do
, matchHeaders = [matchContentTypeJson] , matchHeaders = [matchContentTypeJson]
} }
it "ignores &select when return not set or using return=minimal" $ do
request methodPost "/menagerie?select=integer,varchar" []
[json| [{
"integer": 15, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
}] |] `shouldRespondWith` ""
{ matchStatus = 201 }
request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=minimal")]
[json| [{
"integer": 16, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
}] |] `shouldRespondWith` ""
{ matchStatus = 201 }
context "non uniform json array" $ do context "non uniform json array" $ do
it "rejects json array that isn't exclusivily composed of objects" $ it "rejects json array that isn't exclusivily composed of objects" $
post "/articles" post "/articles"
@@ -148,15 +163,6 @@ spec actualPgVersion = do
simpleBody p `shouldBe` [json| [] |] simpleBody p `shouldBe` [json| [] |]
simpleStatus p `shouldBe` created201 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 it "can post nulls" $ do
p <- request methodPost "/no_pk" p <- request methodPost "/no_pk"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
@@ -198,7 +204,16 @@ spec actualPgVersion = do
it "fails with 400 and error" $ it "fails with 400 and error" $
post "/simple_pk" "}{ x = 2" post "/simple_pk" "}{ x = 2"
`shouldRespondWith` `shouldRespondWith`
[json|{"message":"Error in $: Failed reading: not a valid json value"}|] [json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
context "with no payload" $
it "fails with 400 and error" $
post "/simple_pk" ""
`shouldRespondWith`
[json|{"message":"Error in $: not enough input"}|]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [matchContentTypeJson]
} }
@@ -260,34 +275,18 @@ spec actualPgVersion = do
, matchHeaders = [] , matchHeaders = []
} }
context "table with limited privileges" $ do it "successfully inserts a row with all-default columns with prefer=rep" $
it "succeeds if correct select is applied" $ request methodPost "/items" [("Prefer", "return=representation")] "{}"
request methodPost "/limited_article_stars?select=article_id,user_id" [("Prefer", "return=representation")] `shouldRespondWith` [json|[{ id: 20 }]|]
[json| {"article_id": 2, "user_id": 1} |] `shouldRespondWith` [str|[{"article_id":2,"user_id":1}]|] { matchStatus = 201,
{ matchStatus = 201 matchHeaders = []
, matchHeaders = []
} }
it "fails if more columns are selected" $
request methodPost "/limited_article_stars?select=article_id,user_id,created_at" [("Prefer", "return=representation")] it "successfully inserts a row with all-default columns with prefer=rep and &select=" $
[json| {"article_id": 2, "user_id": 2} |] `shouldRespondWith` ( request methodPost "/items?select=id" [("Prefer", "return=representation")] "{}"
if actualPgVersion >= pgVersion112 then `shouldRespondWith` [json|[{ id: 21 }]|]
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|] { matchStatus = 201,
else matchHeaders = []
[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` (
if actualPgVersion >= pgVersion112 then
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]
else
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]
)
{ matchStatus = 401
, matchHeaders = []
} }
context "POST with ?columns parameter" $ do context "POST with ?columns parameter" $ do
@@ -422,6 +421,24 @@ spec actualPgVersion = do
matchHeaders = [] matchHeaders = []
} }
context "with invalid json payload" $
it "fails with 400 and error" $
request methodPatch "/simple_pk" [] "}{ x = 2"
`shouldRespondWith`
[json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]
{ matchStatus = 400,
matchHeaders = [matchContentTypeJson]
}
context "with no payload" $
it "fails with 400 and error" $
request methodPatch "/items" [] ""
`shouldRespondWith`
[json|{"message":"Error in $: not enough input"}|]
{ matchStatus = 400,
matchHeaders = [matchContentTypeJson]
}
context "in a nonempty table" $ do context "in a nonempty table" $ do
it "can update a single item" $ do it "can update a single item" $ do
g <- get "/items?id=eq.42" g <- get "/items?id=eq.42"
@@ -503,47 +520,123 @@ spec actualPgVersion = do
matchHeaders = [] matchHeaders = []
} }
it "can provide a representation" $ do context "with representation requested" $ do
_ <- post "/items" it "can provide a representation" $ do
[json| { id: 1 } |] _ <- post "/items"
request methodPatch [json| { id: 1 } |]
"/items?id=eq.1" request methodPatch
[("Prefer", "return=representation")] "/items?id=eq.1"
[json| { id: 99 } |] [("Prefer", "return=representation")]
`shouldRespondWith` [json| [{id:99}] |] [json| { id: 99 } |]
{ matchHeaders = [matchContentTypeJson] } `shouldRespondWith` [json| [{id:99}] |]
-- put value back for other tests { matchHeaders = [matchContentTypeJson] }
void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |] -- 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 it "can return computed columns" $
request methodPatch "/items" [] [json| {} |] request methodPatch
"/items?id=eq.1&select=id,always_true"
[("Prefer", "return=representation")]
[json| { id: 1 } |]
`shouldRespondWith` [json| [{ id: 1, always_true: true }] |]
{ matchHeaders = [matchContentTypeJson] }
it "can select overloaded computed columns" $ do
request methodPatch
"/items?id=eq.1&select=id,computed_overload"
[("Prefer", "return=representation")]
[json| { id: 1 } |]
`shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]
{ matchHeaders = [matchContentTypeJson] }
request methodPatch
"/items2?id=eq.1&select=id,computed_overload"
[("Prefer", "return=representation")]
[json| { id: 1 } |]
`shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]
{ matchHeaders = [matchContentTypeJson] }
it "ignores ?select= when return not set or return=minimal" $ do
request methodPatch "/items?id=eq.1&select=id" [] [json| { id:1 } |]
`shouldRespondWith` "" `shouldRespondWith` ""
{ {
matchStatus = 204, matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"] matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
request methodPatch "/items?id=eq.1&select=id" [("Prefer", "return=minimal")] [json| { id:1 } |]
request methodPatch "/items" [] [json| [] |]
`shouldRespondWith` "" `shouldRespondWith` ""
{ {
matchStatus = 204, matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"] matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
request methodPatch "/items" [] [json| [{}] |] context "when patching with an empty body" $ do
`shouldRespondWith` "" it "makes no updates and returns 204 without return= and without ?select=" $ do
{ request methodPatch "/items" [] [json| {} |]
matchStatus = 204, `shouldRespondWith` ""
matchHeaders = ["Content-Range" <:> "*/*"] {
} matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"]
}
it "makes no updates and and returns 200, when patching with an empty json object and return=rep" $ request methodPatch "/items" [] [json| [] |]
request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |] `shouldRespondWith` ""
`shouldRespondWith` "[]" {
{ matchStatus = 204,
matchStatus = 200, matchHeaders = ["Content-Range" <:> "*/*"]
matchHeaders = ["Content-Range" <:> "*/*"] }
}
request methodPatch "/items" [] [json| [{}] |]
`shouldRespondWith` ""
{
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"]
}
it "makes no updates and returns 204 without return= and with ?select=" $ do
request methodPatch "/items?select=id" [] [json| {} |]
`shouldRespondWith` ""
{
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"]
}
request methodPatch "/items?select=id" [] [json| [] |]
`shouldRespondWith` ""
{
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"]
}
request methodPatch "/items?select=id" [] [json| [{}] |]
`shouldRespondWith` ""
{
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"]
}
it "makes no updates and returns 200 with return=rep and without ?select=" $
request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |]
`shouldRespondWith` "[]"
{
matchStatus = 200,
matchHeaders = ["Content-Range" <:> "*/*"]
}
it "makes no updates and returns 200 with return=rep and with ?select=" $
request methodPatch "/items?select=id" [("Prefer", "return=representation")] [json| {} |]
`shouldRespondWith` "[]"
{
matchStatus = 200,
matchHeaders = ["Content-Range" <:> "*/*"]
}
it "makes no updates and returns 200 with return=rep and with ?select= for overloaded computed columns" $
request methodPatch "/items?select=id,computed_overload" [("Prefer", "return=representation")] [json| {} |]
`shouldRespondWith` "[]"
{
matchStatus = 200,
matchHeaders = ["Content-Range" <:> "*/*"]
}
context "with unicode values" $ context "with unicode values" $
it "succeeds and returns values intact" $ do it "succeeds and returns values intact" $ do
@@ -588,3 +681,129 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|] simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|]
simpleStatus p2 `shouldBe` created201 simpleStatus p2 `shouldBe` created201
context "tables with self reference foreign keys" $ do
it "embeds parent after insert" $
request methodPost "/web_content?select=id,name,parent_content:p_web_id(name)"
[("Prefer", "return=representation")]
[json|{"id":6, "name":"wot", "p_web_id":4}|]
`shouldRespondWith`
[json|[{"id":6,"name":"wot","parent_content":{"name":"wut"}}]|]
{ matchStatus = 201
, matchHeaders = [ matchContentTypeJson , "Location" <:> "/web_content?id=eq.6" ]
}
it "embeds children after update" $
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)"
[("Prefer", "return=representation")]
[json|{"name": "tardis-patched"}|]
`shouldRespondWith`
[json|
[ { "id": 0, "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]
|]
{ matchStatus = 200,
matchHeaders = [matchContentTypeJson]
}
it "embeds parent, children and grandchildren after update" $
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)"
[("Prefer", "return=representation")]
[json|{"name": "tardis-patched-2"}|]
`shouldRespondWith`
[json| [
{
"id": 0,
"name": "tardis-patched-2",
"parent_content": { "name": "wat" },
"web_content": [
{ "name": "fezz", "web_content": [ { "name": "wut" } ] },
{ "name": "foo", "web_content": [] },
{ "name": "bar", "web_content": [] }
]
}
] |]
{ matchStatus = 200,
matchHeaders = [matchContentTypeJson]
}
it "embeds children after update without explicitly including the id in the ?select" $
request methodPatch "/web_content?id=eq.0&select=name,web_content(name)"
[("Prefer", "return=representation")]
[json|{"name": "tardis-patched"}|]
`shouldRespondWith`
[json|
[ { "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]
|]
{ matchStatus = 200,
matchHeaders = [matchContentTypeJson]
}
it "embeds an M2M relationship plus parent after update" $
request methodPatch "/users?id=eq.1&select=name,tasks(name,project:projects(name))"
[("Prefer", "return=representation")]
[json|{"name": "Kevin Malone"}|]
`shouldRespondWith`
[json|[
{
"name": "Kevin Malone",
"tasks": [
{ "name": "Design w7", "project": { "name": "Windows 7" } },
{ "name": "Code w7", "project": { "name": "Windows 7" } },
{ "name": "Design w10", "project": { "name": "Windows 10" } },
{ "name": "Code w10", "project": { "name": "Windows 10" } }
]
}
]|]
{ matchStatus = 200,
matchHeaders = [matchContentTypeJson]
}
context "table with limited privileges" $ do
it "succeeds inserting 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 inserting 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` (
if actualPgVersion >= pgVersion112 then
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]
else
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]
)
{ matchStatus = 401
, matchHeaders = []
}
it "fails inserting if select is not specified" $
request methodPost "/limited_article_stars" [("Prefer", "return=representation")]
[json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` (
if actualPgVersion >= pgVersion112 then
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]
else
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]
)
{ matchStatus = 401
, matchHeaders = []
}
it "can insert in a table with no select and return=minimal" $ do
p <- request methodPost "/insertonly"
[("Prefer", "return=minimal")]
[json| { "v":"some value" } |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` created201
it "succeeds updating row and gives a 204 when using return=minimal" $
request methodPatch "/app_users?id=eq.1" [("Prefer", "return=minimal")]
[json| { "password": "passxyz" } |]
`shouldRespondWith` 204
it "can update without return=minimal and no explicit select" $
request methodPatch "/app_users?id=eq.1" []
[json| { "password": "passabc" } |]
`shouldRespondWith` 204
+8 -3
View File
@@ -7,11 +7,11 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import PostgREST.Types (PgVersion, pgVersion112) import PostgREST.Types (PgVersion, pgVersion112, pgVersion121)
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: PgVersion -> SpecWith Application spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion = describe "json and jsonb operators" $ do spec actualPgVersion = describe "json and jsonb operators" $ do
context "Shaping response with select parameter" $ do context "Shaping response with select parameter" $ do
it "obtains a json subfield one level with casting" $ it "obtains a json subfield one level with casting" $
@@ -26,7 +26,12 @@ spec actualPgVersion = describe "json and jsonb operators" $ do
it "fails on bad casting (data of the wrong format)" $ it "fails on bad casting (data of the wrong format)" $
get "/complex_items?select=settings->foo->>bar::integer" get "/complex_items?select=settings->foo->>bar::integer"
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |] `shouldRespondWith` (
if actualPgVersion >= pgVersion121 then
[json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for type integer: \"baz\""} |]
else
[json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
)
{ matchStatus = 400 , matchHeaders = [] } { matchStatus = 400 , matchHeaders = [] }
it "obtains a json subfield two levels (string)" $ it "obtains a json subfield two levels (string)" $
+336
View File
@@ -0,0 +1,336 @@
module Feature.MultipleSchemaSpec where
import Control.Lens ((^?))
import Data.Aeson.Lens
import Data.Aeson.QQ
import Network.HTTP.Types
import Network.Wai (Application)
import Network.Wai.Test (SResponse (simpleHeaders), simpleBody)
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Protolude
import SpecHelper
import PostgREST.Types (PgVersion, pgVersion96)
spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion =
describe "multiple schemas in single instance" $ do
context "Reading tables on different schemas" $ do
it "succeeds in reading table from default schema v1 if no schema is selected via header" $
request methodGet "/parents" [] "" `shouldRespondWith`
[json|[
{"id":1,"name":"parent v1-1"},
{"id":2,"name":"parent v1-2"}
]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
}
it "succeeds in reading table from default schema v1 after explicitly passing it in the header" $
request methodGet "/parents" [("Accept-Profile", "v1")] "" `shouldRespondWith`
[json|[
{"id":1,"name":"parent v1-1"},
{"id":2,"name":"parent v1-2"}
]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
}
it "succeeds in reading table from schema v2" $
request methodGet "/parents" [("Accept-Profile", "v2")] "" `shouldRespondWith`
[json|[
{"id":3,"name":"parent v2-3"},
{"id":4,"name":"parent v2-4"}
]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
it "succeeds in reading another_table from schema v2" $
request methodGet "/another_table" [("Accept-Profile", "v2")] "" `shouldRespondWith`
[json|[
{"id":5,"another_value":"value 5"},
{"id":6,"another_value":"value 6"}
]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
it "doesn't find another_table in schema v1" $
request methodGet "/another_table" [("Accept-Profile", "v1")] "" `shouldRespondWith` 404
it "fails trying to read table from unkown schema" $
request methodGet "/parents" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2"}|]
{
matchStatus = 406
}
context "Inserting tables on different schemas" $ do
it "succeeds inserting on default schema and returning it" $
request methodPost "/children" [("Prefer", "return=representation")] [json|{"name": "child v1-1", "parent_id": 1}|]
`shouldRespondWith`
[json|[{"id":1, "name": "child v1-1", "parent_id": 1}]|]
{
matchStatus = 201
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
}
it "succeeds inserting on the v1 schema and returning its parent" $
request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v1")]
[json|{"name": "child v1-2", "parent_id": 2}|]
`shouldRespondWith`
[json|[{"id":2, "parent": {"id": 2, "name": "parent v1-2"}}]|]
{
matchStatus = 201
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
}
it "succeeds inserting on the v2 schema and returning its parent" $
request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v2")]
[json|{"name": "child v2-3", "parent_id": 3}|]
`shouldRespondWith`
[json|[{"id":1, "parent": {"id": 3, "name": "parent v2-3"}}]|]
{
matchStatus = 201
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
it "fails when inserting on an unknown schema" $
request methodPost "/children" [("Content-Profile", "unknown")]
[json|{"name": "child 4", "parent_id": 4}|]
`shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2"}|]
{
matchStatus = 406
}
context "calling procs on different schemas" $ do
it "succeeds in calling the default schema proc" $
request methodGet "/rpc/get_parents_below?id=6" [] ""
`shouldRespondWith`
[json|[{"id":1,"name":"parent v1-1"}, {"id":2,"name":"parent v1-2"}]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
}
it "succeeds in calling the v1 schema proc and embedding" $
request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v1")] ""
`shouldRespondWith`
[json| [
{"id":1,"name":"parent v1-1","children":[{"id":1,"name":"child v1-1"}]},
{"id":2,"name":"parent v1-2","children":[{"id":2,"name":"child v1-2"}]}] |]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
}
it "succeeds in calling the v2 schema proc and embedding" $
request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v2")] ""
`shouldRespondWith`
[json| [
{"id":3,"name":"parent v2-3","children":[{"id":1,"name":"child v2-3"}]},
{"id":4,"name":"parent v2-4","children":[]}] |]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
it "succeeds in calling the v2 schema proc with POST by using Content-Profile" $
request methodPost "/rpc/get_parents_below?select=id,name" [("Content-Profile", "v2")]
[json|{"id": "6"}|]
`shouldRespondWith`
[json| [
{"id":3,"name":"parent v2-3"},
{"id":4,"name":"parent v2-4"}]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
context "Modifying tables on different schemas" $ do
it "succeeds in patching on the v1 schema and returning its parent" $
request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v1"), ("Prefer", "return=representation")]
[json|{"name": "child v1-1 updated"}|]
`shouldRespondWith`
[json|[{"name":"child v1-1 updated", "parent": {"name": "parent v1-1"}}]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
}
it "succeeds in patching on the v2 schema and returning its parent" $
request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
[json|{"name": "child v2-1 updated"}|]
`shouldRespondWith`
[json|[{"name":"child v2-1 updated", "parent": {"name": "parent v2-3"}}]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
it "succeeds on deleting on the v2 schema" $ do
request methodDelete "/children?id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")] ""
`shouldRespondWith` [json|[{"id": 1, "name": "child v2-1 updated", "parent_id": 3}]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
request methodGet "/children?id=eq.1" [("Accept-Profile", "v2")] ""
`shouldRespondWith` "[]"
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
when (actualPgVersion >= pgVersion96) $
it "succeeds on PUT on the v2 schema" $
request methodPut "/children?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
[json| [ { "id": 111, "name": "child v2-111", "parent_id": null } ]|]
`shouldRespondWith`
[json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
context "OpenAPI output" $ do
it "succeeds in reading table definition from default schema v1 if no schema is selected via header" $ do
req <- request methodGet "/" [] ""
liftIO $ do
simpleHeaders req `shouldSatisfy` matchHeader "Content-Profile" "v1"
let def = simpleBody req ^? key "definitions" . key "parents"
def `shouldBe` Just
[aesonQQ|
{
"type" : "object",
"properties" : {
"id" : {
"description" : "Note:\nThis is a Primary Key.<pk/>",
"format" : "integer",
"type" : "integer"
},
"name" : {
"format" : "text",
"type" : "string"
}
},
"required" : [
"id"
]
}
|]
it "succeeds in reading table definition from default schema v1 after explicitly passing it in the header" $ do
r <- request methodGet "/" [("Accept-Profile", "v1")] ""
liftIO $ do
simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v1"
let def = simpleBody r ^? key "definitions" . key "parents"
def `shouldBe` Just
[aesonQQ|
{
"type" : "object",
"properties" : {
"id" : {
"description" : "Note:\nThis is a Primary Key.<pk/>",
"format" : "integer",
"type" : "integer"
},
"name" : {
"format" : "text",
"type" : "string"
}
},
"required" : [
"id"
]
}
|]
it "succeeds in reading table definition from schema v2" $ do
r <- request methodGet "/" [("Accept-Profile", "v2")] ""
liftIO $ do
simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v2"
let def = simpleBody r ^? key "definitions" . key "parents"
def `shouldBe` Just
[aesonQQ|
{
"type" : "object",
"properties" : {
"id" : {
"description" : "Note:\nThis is a Primary Key.<pk/>",
"format" : "integer",
"type" : "integer"
},
"name" : {
"format" : "text",
"type" : "string"
}
},
"required" : [
"id"
]
}
|]
it "succeeds in reading another_table definition from schema v2" $ do
r <- request methodGet "/" [("Accept-Profile", "v2")] ""
liftIO $ do
simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v2"
let def = simpleBody r ^? key "definitions" . key "another_table"
def `shouldBe` Just
[aesonQQ|
{
"type" : "object",
"properties" : {
"id" : {
"description" : "Note:\nThis is a Primary Key.<pk/>",
"format" : "integer",
"type" : "integer"
},
"another_value" : {
"format" : "text",
"type" : "string"
}
},
"required" : [
"id"
]
}
|]
it "doesn't find another_table definition in schema v1" $ do
r <- request methodGet "/" [("Accept-Profile", "v1")] ""
liftIO $ do
let def = simpleBody r ^? key "definitions" . key "another_table"
def `shouldBe` Nothing
it "fails trying to read definitions from unkown schema" $
request methodGet "/" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2"}|]
{
matchStatus = 406
}
+1 -1
View File
@@ -13,7 +13,7 @@ import Protolude
import SpecHelper import SpecHelper
-- }}} -- }}}
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = describe "server started without JWT secret" $ do spec = describe "server started without JWT secret" $ do
-- this test will stop working 9999999999s after the UNIX EPOCH -- this test will stop working 9999999999s after the UNIX EPOCH
+1 -1
View File
@@ -7,7 +7,7 @@ import Test.Hspec.Wai
import Protolude hiding (get) import Protolude hiding (get)
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "Non existent api schema" $ do describe "Non existent api schema" $ do
it "succeeds when requesting root path" $ it "succeeds when requesting root path" $
+1 -1
View File
@@ -9,7 +9,7 @@ import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = describe "features supported on PostgreSQL 9.5" $ spec = describe "features supported on PostgreSQL 9.5" $
context "json array negative index" $ do context "json array negative index" $ do
it "can select with negative indexes" $ do it "can select with negative indexes" $ do
+97 -3
View File
@@ -1,6 +1,8 @@
module Feature.PgVersion96Spec where module Feature.PgVersion96Spec where
import Network.Wai (Application) import Network.HTTP.Types
import Network.Wai (Application)
import Network.Wai.Test (SResponse (simpleHeaders))
import Test.Hspec import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
@@ -9,10 +11,10 @@ import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "features supported on PostgreSQL 9.6" $ do describe "features supported on PostgreSQL 9.6" $ do
context "GUC headers" $ do context "GUC headers on function calls" $ do
it "succeeds setting the headers" $ do it "succeeds setting the headers" $ do
get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id" get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id"
`shouldRespondWith` [json|[{"id": 2}]|] `shouldRespondWith` [json|[{"id": 2}]|]
@@ -67,6 +69,98 @@ spec =
"Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/", "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/",
"Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]} "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]}
context "GUC headers on all other methods via pre-request" $ do
it "succeeds setting the headers on GET and HEAD" $ do
request methodGet "/items?id=eq.1" [("User-Agent", "MSIE 6.0")] mempty
`shouldRespondWith` [json|[{"id": 1}]|]
{matchHeaders = [
matchContentTypeJson,
"Cache-Control" <:> "no-cache, no-store, must-revalidate"]}
request methodHead "/items?id=eq.1" [("User-Agent", "MSIE 7.0")] mempty
`shouldRespondWith` ""
{matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"]}
request methodHead "/projects" [("Accept", "text/csv")] mempty
`shouldRespondWith` ""
{matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"]}
it "succeeds setting the headers on POST" $
request methodPost "/items" [] [json|[{"id": 11111}]|]
`shouldRespondWith` ""
{ matchStatus = 201
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
it "succeeds setting the headers on PATCH" $
request methodPatch "/items?id=eq.11111" [] [json|[{"id": 11111}]|]
`shouldRespondWith` ""
{ matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
it "succeeds setting the headers on PUT" $
request methodPut "/items?id=eq.11111" [] [json|[{"id": 11111}]|]
`shouldRespondWith` ""
{ matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
it "succeeds setting the headers on DELETE" $
request methodDelete "/items?id=eq.11111" [] mempty
`shouldRespondWith` ""
{ matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
context "Override provided headers by using GUC headers" $ do
it "can override the Content-Type header" $ do
request methodHead "/clients?id=eq.1" [] mempty
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/geo+json"]
}
request methodHead "/rpc/getallprojects" [] mempty
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/geo+json"]
}
it "can override the Location header" $
request methodPost "/stuff" [] [json|[{"id": 1, "name": "stuff 1"}]|]
`shouldRespondWith` ""
{ matchStatus = 201
, matchHeaders = ["Location" <:> "/stuff?id=eq.1&overriden=true"]
}
-- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535
-- it was reported that blank headers ` : ` where added and that cause proxies to fail the requests.
-- These tests are to ensure no blank headers are added.
context "Blank headers bug" $ do
it "shouldn't add blank headers on POST" $ do
r <- request methodPost "/loc_test" [] [json|{"id": "1", "c": "c1"}|]
liftIO $ do
let respHeaders = simpleHeaders r
respHeaders `shouldSatisfy` noBlankHeader
it "shouldn't add blank headers on PATCH" $ do
r <- request methodPatch "/loc_test?id=eq.1" [] [json|{"c": "c2"}|]
liftIO $ do
let respHeaders = simpleHeaders r
respHeaders `shouldSatisfy` noBlankHeader
it "shouldn't add blank headers on GET" $ do
r <- request methodGet "/loc_test" [] ""
liftIO $ do
let respHeaders = simpleHeaders r
respHeaders `shouldSatisfy` noBlankHeader
it "shouldn't add blank headers on DELETE" $ do
r <- request methodDelete "/loc_test?id=eq.1" [] ""
liftIO $ do
let respHeaders = simpleHeaders r
respHeaders `shouldSatisfy` noBlankHeader
context "Use of the phraseto_tsquery function" $ do context "Use of the phraseto_tsquery function" $ do
it "finds matches" $ it "finds matches" $
get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith` get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith`
+1 -1
View File
@@ -6,7 +6,7 @@ import Test.Hspec hiding (pendingWith)
import Protolude import Protolude
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "GET / with proxy" $ describe "GET / with proxy" $
it "returns a valid openapi spec with proxy" $ it "returns a valid openapi spec with proxy" $
+34 -5
View File
@@ -11,9 +11,9 @@ import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "Requesting many items with server limits enabled" $ do describe "Requesting many items with server limits(max-rows) enabled" $ do
it "restricts results" $ it "restricts results" $
get "/items" get "/items"
`shouldRespondWith` [json| [{"id":1},{"id":2}] |] `shouldRespondWith` [json| [{"id":1},{"id":2}] |]
@@ -29,16 +29,45 @@ spec =
matchHeader "Content-Range" "0-0/*" matchHeader "Content-Range" "0-0/*"
simpleStatus r `shouldBe` ok200 simpleStatus r `shouldBe` ok200
it "limit works on all levels" $ it "works on all levels" $
get "/users?select=id,tasks(id)&order=id.asc&tasks.order=id.asc" 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}]}]|] `shouldRespondWith` [json|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
it "limit is not applied to parent embeds" $ it "succeeds in getting parent embeds despite the limit, see #647" $
get "/tasks?select=id,project(id)&id=gt.5" get "/tasks?select=id,project:projects(id)&id=gt.5"
`shouldRespondWith` [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|] `shouldRespondWith` [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
it "can offset the parent embed, being consistent with the other embed types" $
get "/tasks?select=id,project:projects(id)&id=gt.5&project.offset=1"
`shouldRespondWith` [json|[{"id":6,"project":null}, {"id":7,"project":null}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"]
}
context "count=estimated" $ do
it "uses the query planner guess when query rows > maxRows" $
request methodHead "/getallprojects_view" [("Prefer", "count=estimated")] ""
`shouldRespondWith` ""
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/2019"]
}
it "gives exact count when query rows <= maxRows" $
request methodHead "/getallprojects_view?id=lt.3" [("Prefer", "count=estimated")] ""
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
it "only uses the query planner guess if it's indeed greater than the exact count" $
request methodHead "/get_projects_above_view" [("Prefer", "count=estimated")] ""
`shouldRespondWith` ""
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/3"]
}
+63 -272
View File
@@ -4,17 +4,17 @@ import Network.Wai (Application)
import Network.Wai.Test (SResponse (simpleHeaders)) import Network.Wai.Test (SResponse (simpleHeaders))
import Network.HTTP.Types import Network.HTTP.Types
import Test.Hspec import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Text.Heredoc import Text.Heredoc
import PostgREST.Types (PgVersion, pgVersion112) import PostgREST.Types (PgVersion, pgVersion112, pgVersion121)
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: PgVersion -> SpecWith Application spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion = do spec actualPgVersion = do
describe "Querying a table with a column called count" $ describe "Querying a table with a column called count" $
@@ -198,7 +198,7 @@ spec actualPgVersion = do
it "matches filtering nested items 2" $ it "matches filtering nested items 2" $
get "/clients?select=id,projects(id,tasks2(id,name))&projects.tasks.name=like.Design*" get "/clients?select=id,projects(id,tasks2(id,name))&projects.tasks.name=like.Design*"
`shouldRespondWith` [json| {"message":"Could not find foreign keys between these entities, No relation found between projects and tasks2"}|] `shouldRespondWith` [json| {"message":"Could not find foreign keys between these entities. No relationship found between projects and tasks2"}|]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [matchContentTypeJson]
} }
@@ -237,7 +237,6 @@ spec actualPgVersion = do
[json| [{"myId":1}] |] [json| [{"myId":1}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "one simple column with casting (text)" $ it "one simple column with casting (text)" $
get "/complex_items?select=id::text" `shouldRespondWith` get "/complex_items?select=id::text" `shouldRespondWith`
[json| [{"id":"1"},{"id":"2"},{"id":"3"}] |] [json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]
@@ -265,19 +264,8 @@ spec actualPgVersion = do
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] [json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "requesting parent without specifying primary key" $
get "/projects?select=name,client(name)" `shouldRespondWith`
[json|[
{"name":"Windows 7","client":{"name": "Microsoft"}},
{"name":"Windows 10","client":{"name": "Microsoft"}},
{"name":"IOS","client":{"name": "Apple"}},
{"name":"OSX","client":{"name": "Apple"}},
{"name":"Orphan","client":null}
]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting parent and renaming primary key" $ it "requesting parent and renaming primary key" $
get "/projects?select=name,client(clientId:id,name)" `shouldRespondWith` get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith`
[json|[ [json|[
{"name":"Windows 7","client":{"name": "Microsoft", "clientId": 1}}, {"name":"Windows 7","client":{"name": "Microsoft", "clientId": 1}},
{"name":"Windows 10","client":{"name": "Microsoft", "clientId": 1}}, {"name":"Windows 10","client":{"name": "Microsoft", "clientId": 1}},
@@ -295,24 +283,11 @@ spec actualPgVersion = do
[json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"taskId": 6}}]|] [json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"taskId": 6}}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "embed data with two fk pointing to the same table" $
get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping_address_id(id)" `shouldRespondWith`
[str|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping_address_id":{"id":2}}]|]
it "requesting parents and children while renaming them" $ it "requesting parents and children while renaming them" $
get "/projects?id=eq.1&select=myId:id, name, project_client:client_id(*), project_tasks:tasks(id, name)" `shouldRespondWith` get "/projects?id=eq.1&select=myId:id, name, project_client:clients(*), project_tasks:tasks(id, name)" `shouldRespondWith`
[json|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] [json|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "requesting parents two levels up while using FK to specify the link" $
get "/tasks?id=eq.1&select=id,name,project:project_id(id,name,client:client_id(id,name))" `shouldRespondWith`
[str|[{"id":1,"name":"Design w7","project":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]
it "requesting parents two levels up while using FK to specify the link (with rename)" $
get "/tasks?id=eq.1&select=id,name,project:project_id(id,name,client:client_id(id,name))" `shouldRespondWith`
[str|[{"id":1,"name":"Design w7","project":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]
it "requesting parents and filtering parent columns" $ it "requesting parents and filtering parent columns" $
get "/projects?id=eq.1&select=id, name, clients(id)" `shouldRespondWith` get "/projects?id=eq.1&select=id, name, clients(id)" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|] [str|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|]
@@ -351,20 +326,29 @@ spec actualPgVersion = do
[json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|] [json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can embed by FK column name" $ describe "computed columns" $ do
get "/projects?id=in.(1,3)&select=id,name,client_id(id,name)" `shouldRespondWith` it "computed column on table" $
[json|[{"id":1,"name":"Windows 7","client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":{"id":2,"name":"Apple"}}]|] get "/items?id=eq.1&select=id,always_true" `shouldRespondWith`
{ matchHeaders = [matchContentTypeJson] } [json|[{"id":1,"always_true":true}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed by FK column name and select the FK value at the same time, if aliased" $ it "computed column on rpc" $
get "/projects?id=in.(1,3)&select=id,name,client_id,client:client_id(id,name)" `shouldRespondWith` get "/rpc/search?id=1&select=id,always_true" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|] [json|[{"id":1,"always_true":true}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can select by column name sans id" $ it "overloaded computed columns on both tables" $ do
get "/projects?id=in.(1,3)&select=id,name,client_id,client(id,name)" `shouldRespondWith` get "/items?id=eq.1&select=id,computed_overload" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|] [json|[{"id":1,"computed_overload":true}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/items2?id=eq.1&select=id,computed_overload" `shouldRespondWith`
[json|[{"id":1,"computed_overload":true}]|]
{ matchHeaders = [matchContentTypeJson] }
it "overloaded computed column on rpc" $
get "/rpc/search?id=1&select=id,computed_overload" `shouldRespondWith`
[json|[{"id":1,"computed_overload":true}]|]
{ matchHeaders = [matchContentTypeJson] }
describe "view embedding" $ do describe "view embedding" $ do
it "can detect fk relations through views to tables in the public schema" $ it "can detect fk relations through views to tables in the public schema" $
@@ -373,8 +357,8 @@ spec actualPgVersion = do
it "can detect fk relations through materialized views to tables in the public schema" $ it "can detect fk relations through materialized views to tables in the public schema" $
get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200 get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200
it "can request parent without specifying primary key" $ it "can request two parents" $
get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith` get "/articleStars?select=createdAt,article:articles(owner),user:users(name)&limit=1" `shouldRespondWith`
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|] [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
@@ -394,16 +378,16 @@ spec actualPgVersion = do
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "detects parent relations when having many views of a private table" $ do it "detects parent relations when having many views of a private table" $ do
get "/books?select=title,author(name)&id=eq.5" `shouldRespondWith` get "/books?select=title,author:authors(name)&id=eq.5" `shouldRespondWith`
[json|[ { "title": "Farenheit 451", "author": { "name": "Ray Bradbury" } } ]|] [json|[ { "title": "Farenheit 451", "author": { "name": "Ray Bradbury" } } ]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/forties_books?select=title,author(name)&limit=1" `shouldRespondWith` get "/forties_books?select=title,author:authors(name)&limit=1" `shouldRespondWith`
[json|[ { "title": "1984", "author": { "name": "George Orwell" } } ]|] [json|[ { "title": "1984", "author": { "name": "George Orwell" } } ]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/fifties_books?select=title,author(name)&limit=1" `shouldRespondWith` get "/fifties_books?select=title,author:authors(name)&limit=1" `shouldRespondWith`
[json|[ { "title": "The Catcher in the Rye", "author": { "name": "J.D. Salinger" } } ]|] [json|[ { "title": "The Catcher in the Rye", "author": { "name": "J.D. Salinger" } } ]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/sixties_books?select=title,author(name)&limit=1" `shouldRespondWith` get "/sixties_books?select=title,author:authors(name)&limit=1" `shouldRespondWith`
[json|[ { "title": "To Kill a Mockingbird", "author": { "name": "Harper Lee" } } ]|] [json|[ { "title": "To Kill a Mockingbird", "author": { "name": "Harper Lee" } } ]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
@@ -466,7 +450,7 @@ spec actualPgVersion = do
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can embed a view that has group by" $ it "can embed a view that has group by" $
get "/projects_count_grouped_by?select=number_of_projects,client(name)&order=number_of_projects" `shouldRespondWith` get "/projects_count_grouped_by?select=number_of_projects,client:clients(name)&order=number_of_projects" `shouldRespondWith`
[json| [json|
[{"number_of_projects":1,"client":null}, [{"number_of_projects":1,"client":null},
{"number_of_projects":2,"client":{"name":"Microsoft"}}, {"number_of_projects":2,"client":{"name":"Microsoft"}},
@@ -478,49 +462,6 @@ spec actualPgVersion = do
[json| [{"name":"George Orwell","entities":[3, 4],"books":[{"title":"1984"}]}] |] [json| [{"name":"George Orwell","entities":[3, 4],"books":[{"title":"1984"}]}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
describe "path fixed" $ do
it "works when requesting children 2 levels" $
get "/clients?id=eq.1&select=id,projects:projects!client_id(id,tasks(id))" `shouldRespondWith`
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works with parent relation" $
get "/message?select=id,body,sender:person!sender(name),recipient:person!recipient(name)&id=lt.4" `shouldRespondWith`
[json|
[{"id":1,"body":"Hello Jane","sender":{"name":"John"},"recipient":{"name":"Jane"}},
{"id":2,"body":"Hi John","sender":{"name":"Jane"},"recipient":{"name":"John"}},
{"id":3,"body":"How are you doing?","sender":{"name":"John"},"recipient":{"name":"Jane"}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "fails with an unknown relation" $
get "/message?select=id,sender:person.space(name)&id=lt.4" `shouldRespondWith`
[json|{"message":"Could not find foreign keys between these entities, No relation found between message and person"}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson] }
it "works with a parent view relation" $
get "/message?select=id,body,sender:person_detail!sender(name,sent),recipient:person_detail!recipient(name,received)&id=lt.4" `shouldRespondWith`
[json|
[{"id":1,"body":"Hello Jane","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}},
{"id":2,"body":"Hi John","sender":{"name":"Jane","sent":1},"recipient":{"name":"John","received":1}},
{"id":3,"body":"How are you doing?","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "works with many<->many relation" $
get "/tasks?select=id,users:users!users_tasks(id)" `shouldRespondWith`
[json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]
{ matchHeaders = [matchContentTypeJson] }
-- TODO Remove in next major version(7.0)
describe "old dot '.' symbol, deprecated" $
it "still works" $ do
get "/clients?id=eq.1&select=id,projects:projects.client_id(id,tasks(id))" `shouldRespondWith`
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/tasks?select=id,users:users.users_tasks(id)" `shouldRespondWith`
[json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]
{ matchHeaders = [matchContentTypeJson] }
describe "aliased embeds" $ do describe "aliased embeds" $ do
it "works with child relation" $ it "works with child relation" $
get "/space?select=id,zones:zone(id,name),stores:zone(id,name)&zones.zone_type_id=eq.2&stores.zone_type_id=eq.3" `shouldRespondWith` get "/space?select=id,zones:zone(id,name),stores:zone(id,name)&zones.zone_type_id=eq.2&stores.zone_type_id=eq.3" `shouldRespondWith`
@@ -564,128 +505,20 @@ spec actualPgVersion = do
"designTasks":[ ] } "designTasks":[ ] }
]|] { matchHeaders = [matchContentTypeJson] } ]|] { matchHeaders = [matchContentTypeJson] }
it "works with two aliased childs embeds plus and/or" $ it "works with two aliased children embeds plus and/or" $
get "/entities?select=id,childs:child_entities(id,gChilds:grandchild_entities(id))&childs.and=(id.in.(1,2,3))&childs.gChilds.or=(id.eq.1,id.eq.2)" `shouldRespondWith` get "/entities?select=id,children:child_entities(id,gChildren:grandchild_entities(id))&children.and=(id.in.(1,2,3))&children.gChildren.or=(id.eq.1,id.eq.2)" `shouldRespondWith`
[json|[ [json|[
{ "id":1, { "id":1,
"childs":[ "children":[
{"id":1,"gChilds":[{"id":1}, {"id":2}]}, {"id":1,"gChildren":[{"id":1}, {"id":2}]},
{"id":2,"gChilds":[]}]}, {"id":2,"gChildren":[]}]},
{ "id":2, { "id":2,
"childs":[ "children":[
{"id":3,"gChilds":[]}]}, {"id":3,"gChildren":[]}]},
{ "id":3,"childs":[]}, { "id":3,"children":[]},
{ "id":4,"childs":[]} { "id":4,"children":[]}
]|] { matchHeaders = [matchContentTypeJson] } ]|] { matchHeaders = [matchContentTypeJson] }
describe "tables with self reference foreign keys" $ do
context "one self reference foreign key" $ do
it "embeds parents recursively" $
get "/family_tree?id=in.(3,4)&select=id,parent(id,name,parent(*))" `shouldRespondWith`
[json|[
{ "id": "3", "parent": { "id": "1", "name": "Parental Unit", "parent": null } },
{ "id": "4", "parent": { "id": "2", "name": "Kid One", "parent": { "id": "1", "name": "Parental Unit", "parent": null } } }
]|]
{ matchHeaders = [matchContentTypeJson] }
it "embeds childs recursively" $
get "/family_tree?id=eq.1&select=id,name, childs:family_tree!parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith`
[json|[{
"id": "1", "name": "Parental Unit", "childs": [
{ "id": "2", "name": "Kid One", "childs": [ { "id": "4", "name": "Grandkid One" } ] },
{ "id": "3", "name": "Kid Two", "childs": [ { "id": "5", "name": "Grandkid Two" } ] }
]
}]|] { matchHeaders = [matchContentTypeJson] }
it "embeds parent and then embeds childs" $
get "/family_tree?id=eq.2&select=id,name,parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith`
[json|[{
"id": "2", "name": "Kid One", "parent": {
"id": "1", "name": "Parental Unit", "childs": [ { "id": "2", "name": "Kid One" }, { "id": "3", "name": "Kid Two"} ]
}
}]|] { matchHeaders = [matchContentTypeJson] }
context "two self reference foreign keys" $ do
it "embeds parents" $
get "/organizations?select=id,name,referee(id,name),auditor(id,name)&id=eq.3" `shouldRespondWith`
[json|[{
"id": 3, "name": "Acme",
"referee": {
"id": 1,
"name": "Referee Org"
},
"auditor": {
"id": 2,
"name": "Auditor Org"
}
}]|] { matchHeaders = [matchContentTypeJson] }
it "embeds childs" $ do
get "/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1" `shouldRespondWith`
[json|[{
"id": 1, "name": "Referee Org",
"refereeds": [
{
"id": 3,
"name": "Acme"
},
{
"id": 4,
"name": "Umbrella"
}
]
}]|] { matchHeaders = [matchContentTypeJson] }
get "/organizations?select=id,name,auditees:organizations!auditor(id,name)&id=eq.2" `shouldRespondWith`
[json|[{
"id": 2, "name": "Auditor Org",
"auditees": [
{
"id": 3,
"name": "Acme"
},
{
"id": 4,
"name": "Umbrella"
}
]
}]|] { matchHeaders = [matchContentTypeJson] }
it "embeds other relations(manager) besides the self reference" $ do
get "/organizations?select=name,manager(name),referee(name,manager(name),auditor(name,manager(name))),auditor(name,manager(name),referee(name,manager(name)))&id=eq.5" `shouldRespondWith`
[json|[{
"name":"Cyberdyne",
"manager":{"name":"Cyberdyne Manager"},
"referee":{
"name":"Acme",
"manager":{"name":"Acme Manager"},
"auditor":{
"name":"Auditor Org",
"manager":{"name":"Auditor Manager"}}},
"auditor":{
"name":"Umbrella",
"manager":{"name":"Umbrella Manager"},
"referee":{
"name":"Referee Org",
"manager":{"name":"Referee Manager"}}}
}]|] { matchHeaders = [matchContentTypeJson] }
get "/organizations?select=name,manager(name),auditees:organizations!auditor(name,manager(name),refereeds:organizations!referee(name,manager(name)))&id=eq.2" `shouldRespondWith`
[json|[{
"name":"Auditor Org",
"manager":{"name":"Auditor Manager"},
"auditees":[
{"name":"Acme",
"manager":{"name":"Acme Manager"},
"refereeds":[
{"name":"Cyberdyne",
"manager":{"name":"Cyberdyne Manager"}},
{"name":"Oscorp",
"manager":{"name":"Oscorp Manager"}}]},
{"name":"Umbrella",
"manager":{"name":"Umbrella Manager"},
"refereeds":[]}]
}]|] { matchHeaders = [matchContentTypeJson] }
describe "ordering response" $ do describe "ordering response" $ do
it "by a column asc" $ it "by a column asc" $
get "/items?id=lte.2&order=id.asc" get "/items?id=lte.2&order=id.asc"
@@ -781,10 +614,6 @@ spec actualPgVersion = do
get "/projects?id=eq.1&select=id, name, clients(id, name)&clients.order=name.asc" `shouldRespondWith` get "/projects?id=eq.1&select=id, name, clients(id, name)&clients.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"}}]|] [str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"}}]|]
it "ordering embeded parents does not break things when using ducktape names" $
get "/projects?id=eq.1&select=id, name, client(id, name)&client.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}]|]
context "order syntax errors" $ do context "order syntax errors" $ do
it "gives meaningful error messages when asc/desc/nulls{first,last} are misspelled" $ do it "gives meaningful error messages when asc/desc/nulls{first,last} are misspelled" $ do
get "/items?order=id.ac" `shouldRespondWith` get "/items?order=id.ac" `shouldRespondWith`
@@ -914,11 +743,6 @@ spec actualPgVersion = do
[json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":5}]}] |] [json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":5}]}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "will embed using a column" $
get "/ghostBusters?select=escapeId(*)" `shouldRespondWith`
[json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "will select and filter a column that has spaces" $ it "will select and filter a column that has spaces" $
get "/Server%20Today?select=Just%20A%20Server%20Model&Just%20A%20Server%20Model=like.*91*" `shouldRespondWith` get "/Server%20Today?select=Just%20A%20Server%20Model&Just%20A%20Server%20Model=like.*91*" `shouldRespondWith`
[json|[ [json|[
@@ -967,58 +791,6 @@ spec actualPgVersion = do
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"] , matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
} }
describe "HTTP request env vars" $ do
it "custom header is set" $
request methodPost "/rpc/get_guc_value"
[("Custom-Header", "test")]
[json| { "name": "request.header.custom-header" } |]
`shouldRespondWith`
[str|"test"|]
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson ]
}
it "standard header is set" $
request methodPost "/rpc/get_guc_value"
[("Origin", "http://example.com")]
[json| { "name": "request.header.origin" } |]
`shouldRespondWith`
[str|"http://example.com"|]
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson ]
}
it "current role is available as GUC claim" $
request methodPost "/rpc/get_guc_value" []
[json| { "name": "request.jwt.claim.role" } |]
`shouldRespondWith`
[str|"postgrest_test_anonymous"|]
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson ]
}
it "single cookie ends up as claims" $
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]
[json| {"name":"request.cookie.acookie"} |]
`shouldRespondWith`
[str|"cookievalue"|]
{ matchStatus = 200
, matchHeaders = []
}
it "multiple cookies ends up as claims" $
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]
[json| {"name":"request.cookie.secondcookie"} |]
`shouldRespondWith`
[str|"anothervalue"|]
{ matchStatus = 200
, matchHeaders = []
}
it "app settings available" $
request methodPost "/rpc/get_guc_value" []
[json| { "name": "app.settings.app_host" } |]
`shouldRespondWith`
[str|"localhost"|]
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson ]
}
describe "values with quotes in IN and NOT IN" $ do describe "values with quotes in IN and NOT IN" $ do
it "succeeds when only quoted values are present" $ do it "succeeds when only quoted values are present" $ do
get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\")" `shouldRespondWith` get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\")" `shouldRespondWith`
@@ -1084,8 +856,12 @@ spec actualPgVersion = do
it "only returns an empty result set if the in value is empty" $ it "only returns an empty result set if the in value is empty" $
get "/items_with_different_col_types?int_data=in.( ,3,4)" get "/items_with_different_col_types?int_data=in.( ,3,4)"
`shouldRespondWith` `shouldRespondWith` (
if actualPgVersion >= pgVersion121 then
[json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for type integer: \"\""} |]
else
[json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"\""} |] [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"\""} |]
)
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [matchContentTypeJson]
} }
@@ -1112,3 +888,18 @@ spec actualPgVersion = do
it "cannot use ltree(in public schema) extension operators if no extra search path added" $ it "cannot use ltree(in public schema) extension operators if no extra search path added" $
get "/ltree_sample?path=cd.Top.Science.Astronomy" `shouldRespondWith` 400 get "/ltree_sample?path=cd.Top.Science.Astronomy" `shouldRespondWith` 400
context "VIEW that has a source FK based on a UNIQUE key" $
it "can be embedded" $
get "/referrals?select=site,link:pages(url)" `shouldRespondWith`
[json| [
{"site":"github.com", "link":{"url":"http://postgrest.org/en/v6.0/api.html"}},
{"site":"hub.docker.com", "link":{"url":"http://postgrest.org/en/v6.0/admin.html"}}
]|]
{ matchHeaders = [matchContentTypeJson] }
it "shouldn't produce a Content-Profile header since only a single schema is exposed" $ do
r <- get "/items"
liftIO $ do
let respHeaders = simpleHeaders r
respHeaders `shouldSatisfy` noProfileHeader
+66 -2
View File
@@ -19,7 +19,7 @@ defaultRange = [json| { "min": 0, "max": 15 } |]
emptyRange :: BL.ByteString emptyRange :: BL.ByteString
emptyRange = [json| { "min": 2, "max": 2 } |] emptyRange = [json| { "min": 2, "max": 2 } |]
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = do spec = do
describe "POST /rpc/getitemrange" $ do describe "POST /rpc/getitemrange" $ do
context "without range headers" $ do context "without range headers" $ do
@@ -155,12 +155,17 @@ spec = do
, matchHeaders = ["Content-Range" <:> "0-0/*"] , matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
it "limit and offset works on first level" $ it "limit and offset works on first level" $ do
get "/items?select=id&order=id.asc&limit=3&offset=2" get "/items?select=id&order=id.asc&limit=3&offset=2"
`shouldRespondWith` [json|[{"id":3},{"id":4},{"id":5}]|] `shouldRespondWith` [json|[{"id":3},{"id":4},{"id":5}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "2-4/*"] , matchHeaders = ["Content-Range" <:> "2-4/*"]
} }
request methodHead "/items?select=id&order=id.asc&limit=3&offset=2" [] mempty
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "2-4/*"]
}
it "succeeds if offset equals 0 as a no-op" $ it "succeeds if offset equals 0 as a no-op" $
get "/items?select=id&offset=0" get "/items?select=id&offset=0"
@@ -192,6 +197,65 @@ spec = do
, matchHeaders = [matchContentTypeJson] , matchHeaders = [matchContentTypeJson]
} }
context "when count=planned" $ do
it "obtains a filtered range" $ do
request methodGet "/items?select=id&id=gt.8" [("Prefer", "count=planned")] ""
`shouldRespondWith` [json|[{"id":9}, {"id":10}, {"id":11}, {"id":12}, {"id":13}, {"id":14}, {"id":15}]|]
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-6/8"]
}
request methodGet "/child_entities?select=id&id=gt.3" [("Prefer", "count=planned")] ""
`shouldRespondWith` [json|[{"id":4}, {"id":5}, {"id":6}]|]
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-2/4"]
}
request methodGet "/getallprojects_view?select=id&id=lt.3" [("Prefer", "count=planned")] ""
`shouldRespondWith` [json|[{"id":1}, {"id":2}]|]
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/673"]
}
it "obtains the full range" $ do
request methodHead "/items" [("Prefer", "count=planned")] ""
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/15"]
}
request methodHead "/child_entities" [("Prefer", "count=planned")] ""
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-5/6"]
}
request methodHead "/getallprojects_view" [("Prefer", "count=planned")] ""
`shouldRespondWith` ""
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-4/2019"]
}
it "ignores limit/offset on the planned count" $ do
request methodHead "/items?limit=2&offset=3" [("Prefer", "count=planned")] ""
`shouldRespondWith` ""
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "3-4/15"]
}
request methodHead "/child_entities?limit=2" [("Prefer", "count=planned")] ""
`shouldRespondWith` ""
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/6"]
}
request methodHead "/getallprojects_view?limit=2" [("Prefer", "count=planned")] ""
`shouldRespondWith` ""
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/2019"]
}
it "works with two levels" $
request methodHead "/child_entities?select=*,entities(*)" [("Prefer", "count=planned")] ""
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-5/6"]
}
context "with range headers" $ do context "with range headers" $ do
context "of acceptable range" $ do context "of acceptable range" $ do
it "succeeds with partial content" $ do it "succeeds with partial content" $ do
+1 -1
View File
@@ -10,7 +10,7 @@ import Test.Hspec.Wai.JSON
import Protolude import Protolude
import SpecHelper (acceptHdrs) import SpecHelper (acceptHdrs)
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = describe "When raw-media-types config variable is missing or left empty" $ do spec = describe "When raw-media-types config variable is missing or left empty" $ do
let firefoxAcceptHdrs = acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" let firefoxAcceptHdrs = acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
chromeAcceptHdrs = acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" chromeAcceptHdrs = acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"
+1 -1
View File
@@ -11,7 +11,7 @@ import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "root spec function" $ do describe "root spec function" $ do
it "accepts application/openapi+json" $ it "accepts application/openapi+json" $
+139 -12
View File
@@ -17,7 +17,7 @@ import PostgREST.Types (PgVersion, pgVersion100, pgVersion109,
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: PgVersion -> SpecWith Application spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion = spec actualPgVersion =
describe "remote procedure call" $ do describe "remote procedure call" $ do
context "a proc that returns a set" $ do context "a proc that returns a set" $ do
@@ -34,6 +34,12 @@ spec actualPgVersion =
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"] , matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
request methodHead "/rpc/getitemrange?min=2&max=4"
(rangeHdrs (ByteRangeFromTo 0 0)) ""
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
it "includes total count if requested" $ do it "includes total count if requested" $ do
request methodPost "/rpc/getitemrange" request methodPost "/rpc/getitemrange"
@@ -49,6 +55,12 @@ spec actualPgVersion =
{ matchStatus = 206 { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-0/2"] , matchHeaders = ["Content-Range" <:> "0-0/2"]
} }
request methodHead "/rpc/getitemrange?min=2&max=4"
(rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""
`shouldRespondWith` ""
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-0/2"]
}
it "returns proper json" $ do it "returns proper json" $ do
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith` post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
@@ -72,6 +84,12 @@ spec actualPgVersion =
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"] , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
} }
request methodHead "/rpc/getitemrange?min=2&max=4"
(acceptHdrs "text/csv") ""
`shouldRespondWith` ""
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
}
context "unknown function" $ do context "unknown function" $ do
it "returns 404" $ it "returns 404" $
@@ -140,10 +158,10 @@ spec actualPgVersion =
context "foreign entities embedding" $ do context "foreign entities embedding" $ do
it "can embed if related tables are in the exposed schema" $ 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` post "/rpc/getproject?select=id,name,client:clients(id),tasks(id)" [json| { "id": 1} |] `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/rpc/getproject?id=1&select=id,name,client(id),tasks(id)" `shouldRespondWith` get "/rpc/getproject?id=1&select=id,name,client:clients(id),tasks(id)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
@@ -161,6 +179,29 @@ spec actualPgVersion =
`shouldRespondWith` [json|[{"id": 2, "articleStars": [{"userId": 3}]}]|] `shouldRespondWith` [json|[{"id": 2, "articleStars": [{"userId": 3}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can embed an M2M relationship table" $
get "/rpc/getallusers?select=name,tasks(name)&id=gt.1"
`shouldRespondWith` [json|[
{"name":"Michael Scott", "tasks":[{"name":"Design IOS"}, {"name":"Code IOS"}, {"name":"Design OSX"}]},
{"name":"Dwight Schrute","tasks":[{"name":"Design w7"}, {"name":"Design IOS"}]}
]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed an M2M relationship table that has a parent relationship table" $
get "/rpc/getallusers?select=name,tasks(name,project:projects(name))&id=gt.1"
`shouldRespondWith` [json|[
{"name":"Michael Scott","tasks":[
{"name":"Design IOS","project":{"name":"IOS"}},
{"name":"Code IOS","project":{"name":"IOS"}},
{"name":"Design OSX","project":{"name":"OSX"}}
]},
{"name":"Dwight Schrute","tasks":[
{"name":"Design w7","project":{"name":"Windows 7"}},
{"name":"Design IOS","project":{"name":"IOS"}}
]}
]|]
{ matchHeaders = [matchContentTypeJson] }
context "a proc that returns an empty rowset" $ context "a proc that returns an empty rowset" $
it "returns empty json array" $ do it "returns empty json array" $ do
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith` post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
@@ -415,24 +456,34 @@ spec actualPgVersion =
it "ignores json keys not included in ?columns" $ it "ignores json keys not included in ?columns" $
post "/rpc/sayhello?columns=name" post "/rpc/sayhello?columns=name"
[json|{"name": "John", "smth": "here", "other": "stuff", "fake_id": 13}|] `shouldRespondWith` [json|{"name": "John", "smth": "here", "other": "stuff", "fake_id": 13}|]
`shouldRespondWith`
[json|"Hello, John"|] [json|"Hello, John"|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
context "bulk RPC" $ do it "only takes the first object in case of array of objects payload" $
it "works with a scalar function an returns a json array" $
post "/rpc/add_them" post "/rpc/add_them"
[json|[ [json|[
{"a": 1, "b": 2}, {"a": 1, "b": 2},
{"a": 4, "b": 6}, {"a": 4, "b": 6},
{"a": 100, "b": 200} {"a": 100, "b": 200} ]|]
]|] `shouldRespondWith` `shouldRespondWith` "3"
{ matchHeaders = [matchContentTypeJson] }
context "bulk RPC with params=multiple-objects" $ do
it "works with a scalar function an returns a json array" $
request methodPost "/rpc/add_them" [("Prefer", "params=multiple-objects")]
[json|[
{"a": 1, "b": 2},
{"a": 4, "b": 6},
{"a": 100, "b": 200} ]|]
`shouldRespondWith`
[json| [json|
[3, 10, 300] [3, 10, 300]
|] { matchHeaders = [matchContentTypeJson] } |] { matchHeaders = [matchContentTypeJson] }
it "works with a scalar function an returns a json array when posting CSV" $ it "works with a scalar function an returns a json array when posting CSV" $
request methodPost "/rpc/add_them" [("Content-Type", "text/csv")] request methodPost "/rpc/add_them" [("Content-Type", "text/csv"), ("Prefer", "params=multiple-objects")]
"a,b\n1,2\n4,6\n100,200" "a,b\n1,2\n4,6\n100,200"
`shouldRespondWith` `shouldRespondWith`
[json| [json|
@@ -443,11 +494,11 @@ spec actualPgVersion =
} }
it "works with a non-scalar result" $ it "works with a non-scalar result" $
post "/rpc/get_projects_below?select=id,name" request methodPost "/rpc/get_projects_below?select=id,name" [("Prefer", "params=multiple-objects")]
[json|[ [json|[
{"id": 1}, {"id": 1},
{"id": 5} {"id": 5} ]|]
]|] `shouldRespondWith` `shouldRespondWith`
[json| [json|
[{"id":1,"name":"Windows 7"}, [{"id":1,"name":"Windows 7"},
{"id":2,"name":"Windows 10"}, {"id":2,"name":"Windows 10"},
@@ -455,6 +506,82 @@ spec actualPgVersion =
{"id":4,"name":"OSX"}] {"id":4,"name":"OSX"}]
|] { matchHeaders = [matchContentTypeJson] } |] { matchHeaders = [matchContentTypeJson] }
context "HTTP request env vars" $ do
it "custom header is set" $
request methodPost "/rpc/get_guc_value"
[("Custom-Header", "test")]
[json| { "name": "request.header.custom-header" } |]
`shouldRespondWith`
[str|"test"|]
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson ]
}
it "standard header is set" $
request methodPost "/rpc/get_guc_value"
[("Origin", "http://example.com")]
[json| { "name": "request.header.origin" } |]
`shouldRespondWith`
[str|"http://example.com"|]
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson ]
}
it "current role is available as GUC claim" $
request methodPost "/rpc/get_guc_value" []
[json| { "name": "request.jwt.claim.role" } |]
`shouldRespondWith`
[str|"postgrest_test_anonymous"|]
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson ]
}
it "single cookie ends up as claims" $
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]
[json| {"name":"request.cookie.acookie"} |]
`shouldRespondWith`
[str|"cookievalue"|]
{ matchStatus = 200
, matchHeaders = []
}
it "multiple cookies ends up as claims" $
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]
[json| {"name":"request.cookie.secondcookie"} |]
`shouldRespondWith`
[str|"anothervalue"|]
{ matchStatus = 200
, matchHeaders = []
}
it "app settings available" $
request methodPost "/rpc/get_guc_value" []
[json| { "name": "app.settings.app_host" } |]
`shouldRespondWith`
[str|"localhost"|]
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson ]
}
it "gets the Authorization value" $
request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]
[json| {"name":"request.header.authorization"} |]
`shouldRespondWith`
[str|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]
{ matchStatus = 200
, matchHeaders = []
}
it "gets the http method" $
request methodPost "/rpc/get_guc_value" []
[json| {"name":"request.method"} |]
`shouldRespondWith`
[str|"POST"|]
{ matchStatus = 200
, matchHeaders = []
}
it "gets the http path" $
request methodPost "/rpc/get_guc_value" []
[json| {"name":"request.path"} |]
`shouldRespondWith`
[str|"/rpc/get_guc_value"|]
{ matchStatus = 200
, matchHeaders = []
}
context "binary output" $ do context "binary output" $ do
context "Proc that returns scalar" $ do context "Proc that returns scalar" $ do
it "can query without selecting column" $ it "can query without selecting column" $
+85 -18
View File
@@ -13,7 +13,7 @@ import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "Requesting singular json object" $ do describe "Requesting singular json object" $ do
let pgrstObj = "application/vnd.pgrst.object+json" let pgrstObj = "application/vnd.pgrst.object+json"
@@ -56,7 +56,7 @@ spec =
_ <- post "/addresses" [json| { id: 98, address: "xxx" } |] _ <- post "/addresses" [json| { id: 98, address: "xxx" } |]
_ <- post "/addresses" [json| { id: 99, address: "yyy" } |] _ <- post "/addresses" [json| { id: 99, address: "yyy" } |]
p <- request methodPatch "/addresses?id=gt.0" p <- request methodPatch "/addresses?id=gt.0"
[("Prefer", "return=representation"), singular] [singular]
[json| { address: "zzz" } |] [json| { address: "zzz" } |]
liftIO $ do liftIO $ do
simpleStatus p `shouldBe` notAcceptable406 simpleStatus p `shouldBe` notAcceptable406
@@ -65,7 +65,29 @@ spec =
-- the rows should not be updated, either -- the rows should not be updated, either
get "/addresses?id=eq.98" `shouldRespondWith` [str|[{"id":98,"address":"xxx"}]|] get "/addresses?id=eq.98" `shouldRespondWith` [str|[{"id":98,"address":"xxx"}]|]
it "raises an error for multiple rows with return=rep" $ do
_ <- post "/addresses" [json| { id: 100, address: "xxx" } |]
_ <- post "/addresses" [json| { id: 101, 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.100" `shouldRespondWith` [str|[{"id":100,"address":"xxx"}]|]
it "raises an error for zero rows" $ it "raises an error for zero rows" $
request methodPatch "/items?id=gt.0&id=lt.0"
[singular] [json|{"id":1}|]
`shouldRespondWith`
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
{ matchStatus = 406
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
}
it "raises an error for zero rows with return=rep" $
request methodPatch "/items?id=gt.0&id=lt.0" request methodPatch "/items?id=gt.0&id=lt.0"
[("Prefer", "return=representation"), singular] [json|{"id":1}|] [("Prefer", "return=representation"), singular] [json|{"id":1}|]
`shouldRespondWith` `shouldRespondWith`
@@ -79,20 +101,20 @@ spec =
p <- request methodPost p <- request methodPost
"/addresses" "/addresses"
[("Prefer", "return=representation"), singular] [("Prefer", "return=representation"), singular]
[json| [ { id: 100, address: "xxx" } ] |] [json| [ { id: 102, address: "xxx" } ] |]
liftIO $ simpleBody p `shouldBe` [str|{"id":100,"address":"xxx"}|] liftIO $ simpleBody p `shouldBe` [str|{"id":102,"address":"xxx"}|]
it "works for one row even with return=minimal" $ do it "works for one row even with return=minimal" $ do
request methodPost "/addresses" request methodPost "/addresses"
[("Prefer", "return=minimal"), singular] [("Prefer", "return=minimal"), singular]
[json| [ { id: 101, address: "xxx" } ] |] [json| [ { id: 103, address: "xxx" } ] |]
`shouldRespondWith` "" `shouldRespondWith` ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
-- and the element should exist -- and the element should exist
get "/addresses?id=eq.101" get "/addresses?id=eq.103"
`shouldRespondWith` [str|[{"id":101,"address":"xxx"}]|] `shouldRespondWith` [str|[{"id":103,"address":"xxx"}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = [] , matchHeaders = []
} }
@@ -100,23 +122,47 @@ spec =
it "raises an error when attempting to create multiple entities" $ do it "raises an error when attempting to create multiple entities" $ do
p <- request methodPost p <- request methodPost
"/addresses" "/addresses"
[("Prefer", "return=representation"), singular] [singular]
[json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |] [json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]
liftIO $ simpleStatus p `shouldBe` notAcceptable406 liftIO $ simpleStatus p `shouldBe` notAcceptable406
-- the rows should not exist, either -- the rows should not exist, either
get "/addresses?id=eq.200" `shouldRespondWith` "[]" get "/addresses?id=eq.200" `shouldRespondWith` "[]"
it "return=minimal allows request to create multiple elements" $ it "raises an error when attempting to create multiple entities with return=rep" $ do
p <- request methodPost
"/addresses"
[("Prefer", "return=representation"), singular]
[json| [ { id: 202, address: "xxx" }, { id: 203, address: "yyy" } ] |]
liftIO $ simpleStatus p `shouldBe` notAcceptable406
-- the rows should not exist, either
get "/addresses?id=eq.202" `shouldRespondWith` "[]"
it "raises an error regardless of return=minimal" $ do
request methodPost "/addresses" request methodPost "/addresses"
[("Prefer", "return=minimal"), singular] [("Prefer", "return=minimal"), singular]
[json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |] [json| [ { id: 204, address: "xxx" }, { id: 205, address: "yyy" } ] |]
`shouldRespondWith` "" `shouldRespondWith`
{ matchStatus = 201 [str|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
, matchHeaders = ["Content-Range" <:> "*/*"] { matchStatus = 406
} , matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
}
-- the rows should not exist, either
get "/addresses?id=eq.204" `shouldRespondWith` "[]"
it "raises an error when creating zero entities" $ it "raises an error when creating zero entities" $
request methodPost "/addresses"
[singular]
[json| [ ] |]
`shouldRespondWith`
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
{ matchStatus = 406
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
}
it "raises an error when creating zero entities with return=rep" $
request methodPost "/addresses" request methodPost "/addresses"
[("Prefer", "return=representation"), singular] [("Prefer", "return=representation"), singular]
[json| [ ] |] [json| [ ] |]
@@ -134,18 +180,39 @@ spec =
liftIO $ simpleBody p `shouldBe` [str|{"id":11}|] liftIO $ simpleBody p `shouldBe` [str|{"id":11}|]
it "raises an error when attempting to delete multiple entities" $ do it "raises an error when attempting to delete multiple entities" $ do
let firstItems = "/items?id=gt.0&id=lt.11" let firstItems = "/items?id=gt.0&id=lt.6"
request methodDelete firstItems
[singular] ""
`shouldRespondWith` 406
get firstItems
`shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}] |]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-4/*"]
}
it "raises an error when attempting to delete multiple entities with return=rep" $ do
let firstItems = "/items?id=gt.5&id=lt.11"
request methodDelete firstItems request methodDelete firstItems
[("Prefer", "return=representation"), singular] "" [("Prefer", "return=representation"), singular] ""
`shouldRespondWith` 406 `shouldRespondWith` 406
get firstItems get firstItems
`shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |] `shouldRespondWith` [json| [{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-9/*"] , matchHeaders = ["Content-Range" <:> "0-4/*"]
} }
it "raises an error when deleting zero entities" $ it "raises an error when deleting zero entities" $
request methodDelete "/items?id=lt.0"
[singular] ""
`shouldRespondWith`
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
{ matchStatus = 406
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
}
it "raises an error when deleting zero entities with return=rep" $
request methodDelete "/items?id=lt.0" request methodDelete "/items?id=lt.0"
[("Prefer", "return=representation"), singular] "" [("Prefer", "return=representation"), singular] ""
`shouldRespondWith` `shouldRespondWith`
+21 -2
View File
@@ -17,12 +17,14 @@ import PostgREST.Config (docsVersion)
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = do spec = do
describe "OpenAPI" $ do describe "OpenAPI" $ do
it "root path returns a valid openapi spec" $ it "root path returns a valid openapi spec" $ do
validateOpenApiResponse [("Accept", "application/openapi+json")] validateOpenApiResponse [("Accept", "application/openapi+json")]
request methodHead "/" (acceptHdrs "application/openapi+json") ""
`shouldRespondWith` "" { matchStatus = 200 }
it "should respond to openapi request on none root path with 415" $ it "should respond to openapi request on none root path with 415" $
request methodGet "/items" request methodGet "/items"
@@ -229,6 +231,23 @@ spec = do
] ]
|] |]
describe "VIEW that has a source FK based on a UNIQUE key" $
it "includes fk description" $ do
r <- simpleBody <$> get "/"
let referralLink = r ^? key "definitions" . key "referrals" . key "properties" . key "link"
liftIO $
referralLink `shouldBe` Just
[aesonQQ|
{
"format": "integer",
"type": "integer",
"description": "Note:\nThis is a Foreign Key to `pages.link`.<fk table='pages' column='link'/>"
}
|]
describe "PostgreSQL to Swagger Type Mapping" $ do describe "PostgreSQL to Swagger Type Mapping" $ do
it "character varying to string" $ do it "character varying to string" $ do
+2 -3
View File
@@ -1,7 +1,6 @@
module Feature.UnicodeSpec where module Feature.UnicodeSpec where
import Control.Monad (void) import Network.Wai (Application)
import Network.Wai (Application)
import Test.Hspec import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
@@ -10,7 +9,7 @@ import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "Reading and writing to unicode schema and table names" $ describe "Reading and writing to unicode schema and table names" $
it "Can read and write values" $ do it "Can read and write values" $ do
+60 -21
View File
@@ -11,7 +11,7 @@ import Text.Heredoc
import Protolude hiding (get, put) import Protolude hiding (get, put)
import SpecHelper import SpecHelper
spec :: SpecWith Application spec :: SpecWith ((), Application)
spec = spec =
describe "UPSERT" $ do describe "UPSERT" $ do
context "with POST" $ do context "with POST" $ do
@@ -49,6 +49,31 @@ spec =
[json|[]|] `shouldRespondWith` [json|[]|] `shouldRespondWith`
[json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] } [json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
it "INSERTs and UPDATEs rows on single unique key conflict" $
request methodPost "/single_unique?on_conflict=unique_key" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json| [
{ "unique_key": 1, "value": "B" },
{ "unique_key": 2, "value": "C" }
]|] `shouldRespondWith` [json| [
{ "unique_key": 1, "value": "B" },
{ "unique_key": 2, "value": "C" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
}
it "INSERTs and UPDATEs rows on compound unique keys conflict" $
request methodPost "/compound_unique?on_conflict=key1,key2" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json| [
{ "key1": 1, "key2": 1, "value": "B" },
{ "key1": 1, "key2": 2, "value": "C" }
]|] `shouldRespondWith` [json| [
{ "key1": 1, "key2": 1, "value": "B" },
{ "key1": 1, "key2": 2, "value": "C" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
}
context "when Prefer: resolution=ignore-duplicates is specified" $ do context "when Prefer: resolution=ignore-duplicates is specified" $ do
it "INSERTs and ignores rows on pk conflict" $ it "INSERTs and ignores rows on pk conflict" $
@@ -75,6 +100,32 @@ spec =
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson] , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
} }
it "INSERTs and ignores rows on single unique key conflict" $
request methodPost "/single_unique?on_conflict=unique_key" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json| [
{ "unique_key": 1, "value": "B" },
{ "unique_key": 2, "value": "C" },
{ "unique_key": 3, "value": "D" }
]|] `shouldRespondWith` [json| [
{ "unique_key": 3, "value": "D" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
}
it "INSERTs and UPDATEs rows on compound unique keys conflict" $
request methodPost "/compound_unique?on_conflict=key1,key2" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json| [
{ "key1": 1, "key2": 1, "value": "B" },
{ "key1": 1, "key2": 2, "value": "C" },
{ "key1": 1, "key2": 3, "value": "D" }
]|] `shouldRespondWith` [json| [
{ "key1": 1, "key2": 3, "value": "D" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
}
it "succeeds if the table has only PK cols and no other cols" $ do 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")] request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|] [json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]
@@ -129,26 +180,6 @@ spec =
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|] [json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] } { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
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`
[json|{"message":"PUT payload must contain a single row"}|]
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
it "fails if not all columns are specified" $ do
put "/tiobe_pls?name=eq.Go"
[str| [ { "name": "Go" } ]|]
`shouldRespondWith`
[json|{"message":"You must specify all columns in the payload when using PUT"}|]
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
put "/employees?first_name=eq.Susan&last_name=eq.Heidt"
[str| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000" } ]|]
`shouldRespondWith`
[json|{"message":"You must specify all columns in the payload when using PUT"}|]
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
it "rejects every other filter than pk cols eq's" $ do it "rejects every other filter than pk cols eq's" $ do
put "/tiobe_pls?rank=eq.19" put "/tiobe_pls?rank=eq.19"
[str| [ { "name": "Go", "rank": 19 } ]|] [str| [ { "name": "Go", "rank": 19 } ]|]
@@ -229,6 +260,14 @@ spec =
put "/tiobe_pls?name=eq.Go" [str| [ { "name": "Go", "rank": 13 } ]|] `shouldRespondWith` 204 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] } get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json| [ { "name": "Go", "rank": 13 } ]|] { matchHeaders = [matchContentTypeJson] }
it "succeeds if the payload has more than one row, but it only puts the first element" $
request methodPut "/tiobe_pls?name=eq.Go"
[("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]
[str| [ { "name": "Go", "rank": 19 }, { "name": "Swift", "rank": 12 } ] |]
`shouldRespondWith`
[json|{ "name": "Go", "rank": 19 }|]
{ matchStatus = 200 , matchHeaders = [matchContentTypeSingular] }
it "succeeds on table with composite pk" $ do it "succeeds on table with composite pk" $ do
get "/employees?first_name=eq.Susan&last_name=eq.Heidt" get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
`shouldRespondWith` `shouldRespondWith`
+86 -54
View File
@@ -6,16 +6,18 @@ import qualified Hasql.Transaction.Sessions as HT
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
updateAction) updateAction)
import Data.Function (id) import Data.Function (id)
import Data.List.NonEmpty (toList)
import Data.Time.Clock (getCurrentTime) import Data.Time.Clock (getCurrentTime)
import Data.IORef import Data.IORef
import Test.Hspec import Test.Hspec
import PostgREST.App (postgrest) import PostgREST.App (postgrest)
import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure (getDbStructure, getPgVersion) import PostgREST.DbStructure (getDbStructure, getPgVersion)
import PostgREST.Types (DbStructure (..), pgVersion95, import PostgREST.Types (pgVersion95, pgVersion96)
pgVersion96) import Protolude hiding (toList, toS)
import Protolude import Protolude.Conv (toS)
import SpecHelper import SpecHelper
import qualified Feature.AndOrParamsSpec import qualified Feature.AndOrParamsSpec
@@ -26,10 +28,12 @@ import qualified Feature.BinaryJwtSecretSpec
import qualified Feature.ConcurrentSpec import qualified Feature.ConcurrentSpec
import qualified Feature.CorsSpec import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec import qualified Feature.DeleteSpec
import qualified Feature.EmbedDisambiguationSpec
import qualified Feature.ExtraSearchPathSpec import qualified Feature.ExtraSearchPathSpec
import qualified Feature.HtmlRawOutputSpec import qualified Feature.HtmlRawOutputSpec
import qualified Feature.InsertSpec import qualified Feature.InsertSpec
import qualified Feature.JsonOperatorSpec import qualified Feature.JsonOperatorSpec
import qualified Feature.MultipleSchemaSpec
import qualified Feature.NoJwtSpec import qualified Feature.NoJwtSpec
import qualified Feature.NonexistentSchemaSpec import qualified Feature.NonexistentSchemaSpec
import qualified Feature.PgVersion95Spec import qualified Feature.PgVersion95Spec
@@ -49,108 +53,136 @@ import qualified Feature.UpsertSpec
main :: IO () main :: IO ()
main = do main = do
getTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
testDbConn <- getEnvVarWithDefault "POSTGREST_TEST_CONNECTION" "postgres://postgrest_test@localhost/postgrest_test" testDbConn <- getEnvVarWithDefault "POSTGREST_TEST_CONNECTION" "postgres://postgrest_test@localhost/postgrest_test"
setupDb testDbConn
pool <- P.acquire (3, 10, toS testDbConn) pool <- P.acquire (3, 10, toS testDbConn)
result <- P.use pool $ do actualPgVersion <- either (panic.show) id <$> P.use pool getPgVersion
ver <- getPgVersion
HT.transaction HT.ReadCommitted HT.Read $ getDbStructure "test" ver
let dbStructure = either (panic.show) id result refDbStructure <- (newIORef . Just) =<< setupDbStructure pool (configSchemas $ testCfg testDbConn) actualPgVersion
getTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime } let
-- For tests that run with the same refDbStructure
app cfg = return ((), postgrest (cfg testDbConn) refDbStructure pool getTime $ pure ())
refDbStructure <- newIORef $ Just dbStructure -- For tests that run with a different DbStructure(depends on configSchemas)
appDbs cfg = do
dbs <- (newIORef . Just) =<< setupDbStructure pool (configSchemas $ cfg testDbConn) actualPgVersion
return ((), postgrest (cfg testDbConn) dbs pool getTime $ pure ())
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure () let withApp = app testCfg
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure () maxRowsApp = app testMaxRowsCfg
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure () proxyApp = app testProxyCfg
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure () noJwtApp = app testCfgNoJWT
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure () binaryJwtApp = app testCfgBinaryJWT
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure () audJwtApp = app testCfgAudienceJWT
audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool getTime $ pure () asymJwkApp = app testCfgAsymJWK
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool getTime $ pure () asymJwkSetApp = app testCfgAsymJWKSet
asymJwkSetApp = return $ postgrest (testCfgAsymJWKSet testDbConn) refDbStructure pool getTime $ pure () extraSearchPathApp = app testCfgExtraSearchPath
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure () rootSpecApp = app testCfgRootSpec
extraSearchPathApp = return $ postgrest (testCfgExtraSearchPath testDbConn) refDbStructure pool getTime $ pure () htmlRawOutputApp = app testCfgHtmlRawOutput
rootSpecApp = return $ postgrest (testCfgRootSpec testDbConn) refDbStructure pool getTime $ pure () responseHeadersApp = app testCfgResponseHeaders
htmlRawOutputApp = return $ postgrest (testCfgHtmlRawOutput testDbConn) refDbStructure pool getTime $ pure ()
let reset :: IO () unicodeApp = appDbs testUnicodeCfg
nonexistentSchemaApp = appDbs testNonexistentSchemaCfg
multipleSchemaApp = appDbs testMultipleSchemaCfg
let reset, analyze :: IO ()
reset = resetDb testDbConn reset = resetDb testDbConn
analyze = do
analyzeTable testDbConn "items"
analyzeTable testDbConn "child_entities"
actualPgVersion = pgVersion dbStructure
extraSpecs = extraSpecs =
[("Feature.UpsertSpec", Feature.UpsertSpec.spec) | actualPgVersion >= pgVersion95] ++ [("Feature.UpsertSpec", Feature.UpsertSpec.spec) | actualPgVersion >= pgVersion95] ++
[("Feature.PgVersion95Spec", Feature.PgVersion95Spec.spec) | actualPgVersion >= pgVersion95] ++ [("Feature.PgVersion95Spec", Feature.PgVersion95Spec.spec) | actualPgVersion >= pgVersion95]
[("Feature.PgVersion96Spec", Feature.PgVersion96Spec.spec) | actualPgVersion >= pgVersion96]
specs = uncurry describe <$> [ specs = uncurry describe <$> [
("Feature.AuthSpec" , Feature.AuthSpec.spec actualPgVersion) ("Feature.AuthSpec" , Feature.AuthSpec.spec actualPgVersion)
, ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec) , ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec)
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec) , ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
, ("Feature.CorsSpec" , Feature.CorsSpec.spec) , ("Feature.CorsSpec" , Feature.CorsSpec.spec)
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec) , ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion)
, ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion) , ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion)
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion) , ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)
, ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion) , ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion)
, ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion) , ("Feature.StructureSpec" , Feature.StructureSpec.spec)
, ("Feature.RangeSpec" , Feature.RangeSpec.spec) , ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec actualPgVersion)
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec actualPgVersion)
] ++ extraSpecs ] ++ extraSpecs
mutSpecs = uncurry describe <$> [
("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
, ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion)
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
]
hspec $ do hspec $ do
mapM_ (beforeAll_ reset . before withApp) specs -- Only certain Specs need a database reset, this should be used with care as it slows down the whole test suite.
mapM_ (afterAll_ reset . before withApp) mutSpecs
mapM_ (before withApp) specs
-- we analyze to get accurate results from EXPLAIN
beforeAll_ analyze . before withApp $
describe "Feature.RangeSpec" Feature.RangeSpec.spec
-- this test runs with a raw-output-media-types set to text/html -- this test runs with a raw-output-media-types set to text/html
beforeAll_ reset . before htmlRawOutputApp $ before htmlRawOutputApp $
describe "Feature.HtmlRawOutputSpec" Feature.HtmlRawOutputSpec.spec describe "Feature.HtmlRawOutputSpec" Feature.HtmlRawOutputSpec.spec
-- this test runs with a different server flag -- this test runs with a different server flag
beforeAll_ reset . before ltdApp $ before maxRowsApp $
describe "Feature.QueryLimitedSpec" Feature.QueryLimitedSpec.spec describe "Feature.QueryLimitedSpec" Feature.QueryLimitedSpec.spec
-- this test runs with a different schema -- this test runs with a different schema
beforeAll_ reset . before unicodeApp $ before unicodeApp $
describe "Feature.UnicodeSpec" Feature.UnicodeSpec.spec describe "Feature.UnicodeSpec" Feature.UnicodeSpec.spec
-- this test runs with a proxy -- this test runs with a proxy
beforeAll_ reset . before proxyApp $ before proxyApp $
describe "Feature.ProxySpec" Feature.ProxySpec.spec describe "Feature.ProxySpec" Feature.ProxySpec.spec
-- this test runs without a JWT secret -- this test runs without a JWT secret
beforeAll_ reset . before noJwtApp $ before noJwtApp $
describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec
-- this test runs with a binary JWT secret -- this test runs with a binary JWT secret
beforeAll_ reset . before binaryJwtApp $ before binaryJwtApp $
describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec
-- this test runs with a binary JWT secret and an audience claim -- this test runs with a binary JWT secret and an audience claim
beforeAll_ reset . before audJwtApp $ before audJwtApp $
describe "Feature.AudienceJwtSecretSpec" Feature.AudienceJwtSecretSpec.spec describe "Feature.AudienceJwtSecretSpec" Feature.AudienceJwtSecretSpec.spec
-- this test runs with asymmetric JWK -- this test runs with asymmetric JWK
beforeAll_ reset . before asymJwkApp $ before asymJwkApp $
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
-- this test runs with asymmetric JWKSet -- this test runs with asymmetric JWKSet
beforeAll_ reset . before asymJwkSetApp $ before asymJwkSetApp $
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
-- this test runs with a nonexistent db-schema -- this test runs with a nonexistent db-schema
beforeAll_ reset . before nonexistentSchemaApp $ before nonexistentSchemaApp $
describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec
-- this test runs with an extra search path -- this test runs with an extra search path
beforeAll_ reset . before extraSearchPathApp $ before extraSearchPathApp $
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
-- this test runs with a root spec function override -- this test runs with a root spec function override
when (actualPgVersion >= pgVersion96) $ when (actualPgVersion >= pgVersion96) $ do
beforeAll_ reset . before rootSpecApp $ before rootSpecApp $
describe "Feature.RootSpec" Feature.RootSpec.spec describe "Feature.RootSpec" Feature.RootSpec.spec
before responseHeadersApp $
describe "Feature.PgVersion96Spec" Feature.PgVersion96Spec.spec
-- this test runs with multiple schemas
before multipleSchemaApp $
describe "Feature.MultipleSchemaSpec" $ Feature.MultipleSchemaSpec.spec actualPgVersion
where
setupDbStructure pool schemas ver =
either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ getDbStructure (toList schemas) ver)
+76
View File
@@ -0,0 +1,76 @@
module Main where
import Control.Lens ((^?))
import qualified Data.Aeson.Lens as L
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Pool as P
import qualified Hasql.Statement as H
import qualified Hasql.Transaction as HT
import qualified Hasql.Transaction.Sessions as HT
import Text.Heredoc
import Protolude hiding (get, toS)
import Protolude.Conv (toS)
import PostgREST.QueryBuilder (requestToCallProcQuery)
import PostgREST.Types
import SpecHelper (getEnvVarWithDefault)
import Test.Hspec
main :: IO ()
main = do
testDbConn <- getEnvVarWithDefault "POSTGREST_TEST_CONNECTION" "postgres://postgrest_test@localhost/postgrest_test"
pool <- P.acquire (3, 10, toS testDbConn)
hspec $ describe "QueryCost" $
context "call proc query" $ do
it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool [str| {"id": 3} |] $
requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False Nothing []
liftIO $
cost `shouldSatisfy` (< Just 40)
it "should not exceed cost when calling setof composite proc with empty params" $ do
cost <- exec pool mempty $
requestToCallProcQuery (QualifiedIdentifier "test" "getallprojects") [] False Nothing []
liftIO $
cost `shouldSatisfy` (< Just 30)
it "should not exceed cost when calling scalar proc" $ do
cost <- exec pool [str| {"a": 3, "b": 4} |] $
requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing []
liftIO $
cost `shouldSatisfy` (< Just 10)
context "params=multiple-objects" $ do
it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool [str| [{"id": 1}, {"id": 4}] |] $
requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False (Just MultipleObjects) []
liftIO $ do
cost `shouldSatisfy` (> Just 2000)
cost `shouldSatisfy` (< Just 2100)
it "should not exceed cost when calling scalar proc" $ do
cost <- exec pool [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |] $
requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing []
liftIO $
cost `shouldSatisfy` (< Just 10)
exec :: P.Pool -> ByteString -> SqlQuery -> IO (Maybe Int64)
exec pool input query =
join . rightToMaybe <$>
P.use pool (HT.transaction HT.ReadCommitted HT.Read $ HT.statement input $ explainCost query)
explainCost :: SqlQuery -> H.Statement ByteString (Maybe Int64)
explainCost query =
H.Statement (encodeUtf8 sql) (HE.param $ HE.nonNullable HE.unknown) decodeExplain False
where
sql = "EXPLAIN (FORMAT JSON) " <> query
decodeExplain :: HD.Result (Maybe Int64)
decodeExplain =
let row = HD.singleRow $ HD.column $ HD.nonNullable HD.bytea in
(^? L.nth 0 . L.key "Plan" . L.key "Total Cost" . L._Integral) <$> row
+30 -21
View File
@@ -7,10 +7,10 @@ import qualified Data.Map.Strict as M
import qualified Data.Set as S import qualified Data.Set as S
import qualified System.IO.Error as E import qualified System.IO.Error as E
import Control.Monad (void)
import Data.Aeson (Value (..), decode, encode) import Data.Aeson (Value (..), decode, encode)
import Data.CaseInsensitive (CI (..)) import Data.CaseInsensitive (CI (..))
import Data.List (lookup) import Data.List (lookup)
import Data.List.NonEmpty (fromList)
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus)) import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
import System.Environment (getEnv) import System.Environment (getEnv)
import System.Process (readProcess) import System.Process (readProcess)
@@ -23,8 +23,9 @@ import Test.Hspec.Wai
import Text.Heredoc import Text.Heredoc
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.Types (JSPathExp (..), QualifiedIdentifier (..)) import PostgREST.Types (JSPathExp (..))
import Protolude import Protolude hiding (toS)
import Protolude.Conv (toS)
matchContentTypeJson :: MatchHeader matchContentTypeJson :: MatchHeader
matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8" matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8"
@@ -32,7 +33,7 @@ matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8"
matchContentTypeSingular :: MatchHeader matchContentTypeSingular :: MatchHeader
matchContentTypeSingular = "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8" matchContentTypeSingular = "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"
validateOpenApiResponse :: [Header] -> WaiSession () validateOpenApiResponse :: [Header] -> WaiSession () ()
validateOpenApiResponse headers = do validateOpenApiResponse headers = do
r <- request methodGet "/" headers "" r <- request methodGet "/" headers ""
liftIO $ liftIO $
@@ -63,9 +64,11 @@ getEnvVarWithDefault var def = toS <$>
_baseCfg :: AppConfig _baseCfg :: AppConfig
_baseCfg = -- Connection Settings _baseCfg = -- Connection Settings
AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000 AppConfig mempty "postgrest_test_anonymous" Nothing (fromList ["test"]) "localhost" 3000
-- No user configured Unix Socket -- No user configured Unix Socket
Nothing Nothing
-- No user configured Unix Socket file mode (defaults to 660)
(Right 432)
-- Jwt settings -- Jwt settings
(Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False Nothing (Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False Nothing
-- Connection Modifiers -- Connection Modifiers
@@ -91,13 +94,13 @@ testCfgNoJWT :: Text -> AppConfig
testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing } testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing }
testUnicodeCfg :: Text -> AppConfig testUnicodeCfg :: Text -> AppConfig
testUnicodeCfg testDbConn = (testCfg testDbConn) { configSchema = "تست" } testUnicodeCfg testDbConn = (testCfg testDbConn) { configSchemas = fromList ["تست"] }
testLtdRowsCfg :: Text -> AppConfig testMaxRowsCfg :: Text -> AppConfig
testLtdRowsCfg testDbConn = (testCfg testDbConn) { configMaxRows = Just 2 } testMaxRowsCfg testDbConn = (testCfg testDbConn) { configMaxRows = Just 2 }
testProxyCfg :: Text -> AppConfig testProxyCfg :: Text -> AppConfig
testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" } testProxyCfg testDbConn = (testCfg testDbConn) { configOpenAPIProxyUri = Just "https://postgrest.com/openapi.json" }
testCfgBinaryJWT :: Text -> AppConfig testCfgBinaryJWT :: Text -> AppConfig
testCfgBinaryJWT testDbConn = (testCfg testDbConn) { testCfgBinaryJWT testDbConn = (testCfg testDbConn) {
@@ -125,33 +128,33 @@ testCfgAsymJWKSet testDbConn = (testCfg testDbConn) {
} }
testNonexistentSchemaCfg :: Text -> AppConfig testNonexistentSchemaCfg :: Text -> AppConfig
testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configSchema = "nonexistent" } testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configSchemas = fromList ["nonexistent"] }
testCfgExtraSearchPath :: Text -> AppConfig testCfgExtraSearchPath :: Text -> AppConfig
testCfgExtraSearchPath testDbConn = (testCfg testDbConn) { configExtraSearchPath = ["public", "extensions"] } testCfgExtraSearchPath testDbConn = (testCfg testDbConn) { configExtraSearchPath = ["public", "extensions"] }
testCfgRootSpec :: Text -> AppConfig testCfgRootSpec :: Text -> AppConfig
testCfgRootSpec testDbConn = (testCfg testDbConn) { configRootSpec = Just $ QualifiedIdentifier "test" "root"} testCfgRootSpec testDbConn = (testCfg testDbConn) { configRootSpec = Just "root"}
testCfgHtmlRawOutput :: Text -> AppConfig testCfgHtmlRawOutput :: Text -> AppConfig
testCfgHtmlRawOutput testDbConn = (testCfg testDbConn) { configRawMediaTypes = ["text/html"] } testCfgHtmlRawOutput testDbConn = (testCfg testDbConn) { configRawMediaTypes = ["text/html"] }
setupDb :: Text -> IO () testCfgResponseHeaders :: Text -> AppConfig
setupDb dbConn = do testCfgResponseHeaders testDbConn = (testCfg testDbConn) { configReqCheck = Just "custom_headers" }
loadFixture dbConn "database"
loadFixture dbConn "roles" testMultipleSchemaCfg :: Text -> AppConfig
loadFixture dbConn "schema" testMultipleSchemaCfg testDbConn = (testCfg testDbConn) { configSchemas = fromList ["v1", "v2"] }
loadFixture dbConn "jwt"
loadFixture dbConn "jsonschema"
loadFixture dbConn "privileges"
resetDb dbConn
resetDb :: Text -> IO () resetDb :: Text -> IO ()
resetDb dbConn = loadFixture dbConn "data" resetDb dbConn = loadFixture dbConn "data"
analyzeTable :: Text -> Text -> IO ()
analyzeTable dbConn tableName =
void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", toS dbConn, "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] []
loadFixture :: Text -> FilePath -> IO() loadFixture :: Text -> FilePath -> IO()
loadFixture dbConn name = loadFixture dbConn name =
void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", toS dbConn, "-a", "-f", "test/fixtures/" ++ name ++ ".sql"] [] void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", toS dbConn, "-q", "-f", "test/fixtures/" ++ name ++ ".sql"] []
rangeHdrs :: ByteRange -> [Header] rangeHdrs :: ByteRange -> [Header]
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)] rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
@@ -169,6 +172,12 @@ matchHeader :: CI BS.ByteString -> BS.ByteString -> [Header] -> Bool
matchHeader name valRegex headers = matchHeader name valRegex headers =
maybe False (=~ valRegex) $ lookup name headers maybe False (=~ valRegex) $ lookup name headers
noBlankHeader :: [Header] -> Bool
noBlankHeader = notElem mempty
noProfileHeader :: [Header] -> Bool
noProfileHeader headers = isNothing $ find ((== "Content-Profile") . fst) headers
authHeaderBasic :: BS.ByteString -> BS.ByteString -> Header authHeaderBasic :: BS.ByteString -> BS.ByteString -> Header
authHeaderBasic u p = authHeaderBasic u p =
(hAuthorization, "Basic " <> (toS . B64.encode . toS $ u <> ":" <> p)) (hAuthorization, "Basic " <> (toS . B64.encode . toS $ u <> ":" <> p))
+2
View File
@@ -55,6 +55,8 @@ ALTER DATABASE $DB SET LC_MESSAGES = 'POSIX';
ALTER DATABASE $DB SET LC_MONETARY = 'POSIX'; ALTER DATABASE $DB SET LC_MONETARY = 'POSIX';
ALTER DATABASE $DB SET LC_NUMERIC = 'POSIX'; ALTER DATABASE $DB SET LC_NUMERIC = 'POSIX';
ALTER DATABASE $DB SET LC_TIME = 'POSIX'; ALTER DATABASE $DB SET LC_TIME = 'POSIX';
\i $BASEPATH/fixtures/load.sql
EOF EOF
#Remove database path from the connection uri--prevents setting up the new database name with PGDATABASE #Remove database path from the connection uri--prevents setting up the new database name with PGDATABASE
+30
View File
@@ -0,0 +1,30 @@
version: "3.6"
services:
test:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
- postgres
working_dir: /root/postgrest
volumes:
- ../:/root/postgrest
- stack-linux:/root/.stack
- stack-work:/root/postgrest/.stack-work
command: bash -c "POSTGREST_TEST_CONNECTION=$$(test/create_test_db 'postgres://postgres:postgres@postgres' test_db) stack test"
postgres:
image: postgres:12.2
environment:
POSTGRES_PASSWORD: postgres
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
driver_opts:
type: tmpfs
device: tmpfs
stack-linux:
stack-work:
+111
View File
@@ -207,6 +207,35 @@ INSERT INTO items VALUES (15);
SELECT pg_catalog.setval('items_id_seq', 15, true); SELECT pg_catalog.setval('items_id_seq', 15, true);
--
-- Data for Name: items2; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE items2 CASCADE;
INSERT INTO items2 VALUES (1);
INSERT INTO items2 VALUES (2);
INSERT INTO items2 VALUES (3);
INSERT INTO items2 VALUES (4);
INSERT INTO items2 VALUES (5);
INSERT INTO items2 VALUES (6);
INSERT INTO items2 VALUES (7);
INSERT INTO items2 VALUES (8);
INSERT INTO items2 VALUES (9);
INSERT INTO items2 VALUES (10);
INSERT INTO items2 VALUES (11);
INSERT INTO items2 VALUES (12);
INSERT INTO items2 VALUES (13);
INSERT INTO items2 VALUES (14);
INSERT INTO items2 VALUES (15);
--
-- Name: items_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -
--
SELECT pg_catalog.setval('items2_id_seq', 15, true);
-- --
-- Data for Name: json; Type: TABLE DATA; Schema: test; Owner: - -- Data for Name: json; Type: TABLE DATA; Schema: test; Owner: -
-- --
@@ -344,6 +373,12 @@ INSERT INTO employees VALUES
TRUNCATE TABLE tiobe_pls CASCADE; TRUNCATE TABLE tiobe_pls CASCADE;
INSERT INTO tiobe_pls VALUES ('Java', 1), ('C', 2), ('Python', 4); INSERT INTO tiobe_pls VALUES ('Java', 1), ('C', 2), ('Python', 4);
TRUNCATE TABLE single_unique CASCADE;
INSERT INTO single_unique (unique_key, value) VALUES (1, 'A');
TRUNCATE TABLE compound_unique CASCADE;
INSERT INTO compound_unique (key1, key2, value) VALUES (1, 1, 'A');
TRUNCATE TABLE only_pk CASCADE; TRUNCATE TABLE only_pk CASCADE;
INSERT INTO only_pk VALUES (1), (2); INSERT INTO only_pk VALUES (1), (2);
@@ -499,3 +534,79 @@ COPY pgrst_reserved_chars ("*id*", ":arr->ow::cast", "(inside,parens)", "a.dotte
2 | arrow-2 | parens-2 | dotted-2 | space-2 2 | arrow-2 | parens-2 | dotted-2 | space-2
3 | arrow-3 | parens-3 | dotted-3 | space-3 3 | arrow-3 | parens-3 | dotted-3 | space-3
\. \.
TRUNCATE TABLE web_content CASCADE;
INSERT INTO web_content VALUES (5, 'wat', null);
INSERT INTO web_content VALUES (0, 'tardis', 5);
INSERT INTO web_content VALUES (1, 'fezz', 0);
INSERT INTO web_content VALUES (2, 'foo', 0);
INSERT INTO web_content VALUES (3, 'bar', 0);
INSERT INTO web_content VALUES (4, 'wut', 1);
TRUNCATE TABLE app_users CASCADE;
INSERT INTO app_users (id, email, "password") VALUES (1, 'test@123.com','pass');
INSERT INTO app_users (id, email, "password") VALUES (2, 'abc@123.com','pass');
INSERT INTO app_users (id, email, "password") VALUES (3, 'def@123.com','pass');
TRUNCATE TABLE private.pages CASCADE;
INSERT INTO private.pages VALUES (1, 'http://postgrest.org/en/v6.0/api.html');
INSERT INTO private.pages VALUES (2, 'http://postgrest.org/en/v6.0/admin.html');
TRUNCATE TABLE private.referrals CASCADE;
INSERT INTO private.referrals VALUES ('github.com', 1);
INSERT INTO private.referrals VALUES ('hub.docker.com', 2);
TRUNCATE TABLE big_projects CASCADE;
INSERT INTO big_projects (big_project_id, name)
VALUES (1, 'big project 1'),
(2, 'big project 2');
TRUNCATE TABLE sites CASCADE;
INSERT INTO sites (site_id, name, main_project_id)
VALUES (1, 'site 1', 1),
(2, 'site 2', null),
(3, 'site 3', 2),
(4, 'site 4', null);
TRUNCATE TABLE jobs CASCADE;
INSERT INTO jobs (job_id, name, site_id, big_project_id)
VALUES ('bc5d5362-b881-438f-b9f5-7417e08704ed', 'job 1-1', 1, 1),
('3bd52697-033b-4edd-8a28-46a9c04b7c1e', 'job 2-1', 2, 1),
('e6e67e4e-19b1-11e9-ab14-d663bd873d93', 'job 2-2', 2, 2);
TRUNCATE TABLE departments CASCADE;
TRUNCATE TABLE agents CASCADE;
INSERT INTO agents (id, name)
VALUES (1, 'agent 1'),
(2, 'agent 2'),
(3, 'agent 3'),
(4, 'agent 4');
INSERT INTO departments (id, name, head_id)
VALUES (1, 'dep 1', 1),
(2, 'dep 3', 3);
UPDATE agents SET department_id = 1 WHERE id in (1, 2);
UPDATE agents SET department_id = 2 WHERE id in (3, 4);
TRUNCATE TABLE schedules CASCADE;
INSERT INTO schedules VALUES(1, 'morning', '06:00:00', '11:59:00');
INSERT INTO schedules VALUES(2, 'afternoon', '12:00:00', '17:59:00');
INSERT INTO schedules VALUES(3, 'night', '18:00:00', '23:59:00');
INSERT INTO schedules VALUES(4, 'early morning', '00:00:00', '05:59:00');
TRUNCATE TABLE activities CASCADE;
INSERT INTO activities(id, schedule_id, car_id) VALUES(1, 1, 'CAR-349');
INSERT INTO activities(id, schedule_id, camera_id) VALUES(2, 3, 'CAM-123');
TRUNCATE TABLE unit_workdays CASCADE;
INSERT INTO unit_workdays VALUES(1, '2019-12-02', 1, 1, 2, 3);
TRUNCATE TABLE v1.parents CASCADE;
INSERT INTO v1.parents VALUES(1, 'parent v1-1'), (2, 'parent v1-2');
TRUNCATE TABLE v2.parents CASCADE;
INSERT INTO v2.parents VALUES(3, 'parent v2-3'), (4, 'parent v2-4');
TRUNCATE TABLE v2.another_table CASCADE;
INSERT INTO v2.another_table VALUES(5, 'value 5'), (6, 'value 6');
+1 -1
View File
@@ -1,3 +1,3 @@
set client_min_messages to warning; set client_min_messages to warning;
DROP SCHEMA IF EXISTS test, private, postgrest, jwt, public, تست, extensions CASCADE; DROP SCHEMA IF EXISTS test, private, postgrest, jwt, public, تست, extensions, v1, v2 CASCADE;
DROP TYPE IF EXISTS jwt_token CASCADE; DROP TYPE IF EXISTS jwt_token CASCADE;
+1 -1
View File
@@ -20,7 +20,7 @@ HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
MODIFICATIONS. MODIFICATIONS.
*/ */
CREATE OR REPLACE FUNCTION _validate_json_schema_type(type text, data jsonb) RETURNS boolean AS $f$ CREATE OR REPLACE FUNCTION public._validate_json_schema_type(type text, data jsonb) RETURNS boolean AS $f$
BEGIN BEGIN
IF type = 'integer' THEN IF type = 'integer' THEN
IF jsonb_typeof(data) != 'number' THEN IF jsonb_typeof(data) != 'number' THEN
+3
View File
@@ -1,5 +1,8 @@
-- From michelp/pgjwt commit c02bbd3 -- From michelp/pgjwt commit c02bbd3
BEGIN; BEGIN;
set search_path to public;
set client_min_messages to warning; set client_min_messages to warning;
DROP SCHEMA IF EXISTS jwt CASCADE; DROP SCHEMA IF EXISTS jwt CASCADE;
CREATE SCHEMA jwt; CREATE SCHEMA jwt;
+11
View File
@@ -0,0 +1,11 @@
-- Loads all fixtures for the PostgREST tests
\set ON_ERROR_STOP on
\ir database.sql
\ir roles.sql
\ir schema.sql
\ir jwt.sql
\ir jsonschema.sql
\ir privileges.sql
\ir data.sql
+35
View File
@@ -6,6 +6,8 @@ GRANT USAGE ON SCHEMA
, public , public
, "تست" , "تست"
, extensions , extensions
, v1
, v2
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
-- Schema test objects -- Schema test objects
@@ -13,6 +15,7 @@ SET search_path = test, "تست", pg_catalog;
GRANT ALL ON TABLE GRANT ALL ON TABLE
items items
, items2
, "articleStars" , "articleStars"
, articles , articles
, auto_incrementing_pk , auto_incrementing_pk
@@ -66,6 +69,8 @@ GRANT ALL ON TABLE
, perf_articles , perf_articles
, employees , employees
, tiobe_pls , tiobe_pls
, single_unique
, compound_unique
, only_pk , only_pk
, family_tree , family_tree
, managers , managers
@@ -101,6 +106,30 @@ GRANT ALL ON TABLE
, pgrst_reserved_chars , pgrst_reserved_chars
, authors_w_entities , authors_w_entities
, openapi_types , openapi_types
, getallprojects_view
, get_projects_above_view
, web_content
, pages
, referrals
, big_projects
, sites
, jobs
, main_jobs
, whatev_projects
, whatev_sites
, whatev_jobs
, agents
, departments
, schedules
, activities
, unit_workdays
, stuff
, loc_test
, v1.parents
, v2.parents
, v2.another_table
, v1.children
, v2.children
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
@@ -110,6 +139,8 @@ GRANT USAGE ON SEQUENCE
, items_id_seq , items_id_seq
, callcounter_count , callcounter_count
, leak_id_seq , leak_id_seq
, v1.children_id_seq
, v2.children_id_seq
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
-- Privileges for non anonymous users -- Privileges for non anonymous users
@@ -120,6 +151,10 @@ GRANT SELECT (article_id, user_id) ON TABLE limited_article_stars TO postgrest_t
GRANT INSERT (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; GRANT UPDATE (article_id, user_id) ON TABLE limited_article_stars TO postgrest_test_anonymous;
GRANT SELECT(id, email) ON TABLE app_users TO postgrest_test_anonymous;
GRANT INSERT, UPDATE ON TABLE app_users TO postgrest_test_anonymous;
GRANT DELETE ON TABLE app_users TO postgrest_test_anonymous;
REVOKE EXECUTE ON FUNCTION privileged_hello(text) FROM PUBLIC; -- All functions are available to every role(PUBLIC) by default 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 EXECUTE ON FUNCTION privileged_hello(text) TO postgrest_test_author;
+334 -309
View File
@@ -18,6 +18,8 @@ CREATE SCHEMA private;
CREATE SCHEMA test; CREATE SCHEMA test;
CREATE SCHEMA تست; CREATE SCHEMA تست;
CREATE SCHEMA extensions; CREATE SCHEMA extensions;
CREATE SCHEMA v1;
CREATE SCHEMA v2;
-- --
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - -- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
@@ -103,19 +105,32 @@ SET default_tablespace = '';
SET default_with_oids = false; SET default_with_oids = false;
--
-- Name: items; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE items ( CREATE TABLE items (
id bigint NOT NULL id bigserial primary key
); );
CREATE TABLE items2 (
id bigserial primary key
);
CREATE FUNCTION search(id BIGINT) RETURNS SETOF items
LANGUAGE plpgsql
AS $$BEGIN
RETURN QUERY SELECT items.id FROM items WHERE items.id=search.id;
END$$;
CREATE FUNCTION always_true(test.items) RETURNS boolean CREATE FUNCTION always_true(test.items) RETURNS boolean
LANGUAGE sql STABLE LANGUAGE sql STABLE
AS $$ SELECT true $$; AS $$ SELECT true $$;
CREATE FUNCTION computed_overload(test.items) RETURNS boolean
LANGUAGE sql STABLE
AS $$ SELECT true $$;
CREATE FUNCTION computed_overload(test.items2) RETURNS boolean
LANGUAGE sql STABLE
AS $$ SELECT true $$;
CREATE FUNCTION is_first(test.items) RETURNS boolean CREATE FUNCTION is_first(test.items) RETURNS boolean
LANGUAGE sql STABLE LANGUAGE sql STABLE
AS $$ SELECT $1.id = 1 $$; AS $$ SELECT $1.id = 1 $$;
@@ -364,59 +379,8 @@ CREATE TABLE auth (
pass character(60) NOT NULL pass character(60) NOT NULL
); );
SET search_path = private, pg_catalog;
--
-- Name: article_stars; Type: TABLE; Schema: private; Owner: -
--
CREATE TABLE article_stars (
article_id integer NOT NULL,
user_id integer NOT NULL,
created_at timestamp without time zone DEFAULT now() NOT NULL
);
--
-- Name: articles; Type: TABLE; Schema: private; Owner: -
--
CREATE TABLE articles (
id integer NOT NULL,
body text,
owner name NOT NULL
);
SET search_path = test, pg_catalog; SET search_path = test, pg_catalog;
CREATE VIEW limited_article_stars AS
SELECT article_id, user_id, created_at FROM private.article_stars;
--
-- Name: articleStars; Type: VIEW; Schema: test; Owner: -
--
CREATE VIEW "articleStars" AS
SELECT article_stars.article_id AS "articleId",
article_stars.user_id AS "userId",
article_stars.created_at AS "createdAt"
FROM private.article_stars;
--
-- Name: articles; Type: VIEW; Schema: test; Owner: -
--
CREATE VIEW articles AS
SELECT articles.id,
articles.body,
articles.owner
FROM private.articles;
-- --
-- Name: authors_only; Type: TABLE; Schema: test; Owner: - -- Name: authors_only; Type: TABLE; Schema: test; Owner: -
-- --
@@ -463,24 +427,10 @@ ALTER SEQUENCE auto_incrementing_pk_id_seq OWNED BY auto_incrementing_pk.id;
-- --
CREATE TABLE clients ( CREATE TABLE clients (
id integer NOT NULL, id integer primary key,
name text NOT NULL name text NOT NULL
); );
--
-- Name: comments; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE comments (
id integer NOT NULL,
commenter_id integer NOT NULL,
user_id integer NOT NULL,
task_id integer NOT NULL,
content text NOT NULL
);
-- --
-- Name: complex_items; Type: TABLE; Schema: test; Owner: - -- Name: complex_items; Type: TABLE; Schema: test; Owner: -
-- --
@@ -574,26 +524,6 @@ CREATE VIEW insertable_view_with_join AS
FROM (has_fk FROM (has_fk
JOIN auto_incrementing_pk USING (id)); JOIN auto_incrementing_pk USING (id));
--
-- Name: items_id_seq; Type: SEQUENCE; Schema: test; Owner: -
--
CREATE SEQUENCE items_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: items_id_seq; Type: SEQUENCE OWNED BY; Schema: test; Owner: -
--
ALTER SEQUENCE items_id_seq OWNED BY items.id;
-- --
-- Name: json; Type: TABLE; Schema: test; Owner: - -- Name: json; Type: TABLE; Schema: test; Owner: -
-- --
@@ -663,11 +593,11 @@ CREATE TABLE insertonly (
-- --
CREATE TABLE projects ( CREATE TABLE projects (
id integer NOT NULL, id integer primary key,
name text NOT NULL, name text NOT NULL,
client_id integer client_id integer REFERENCES clients(id)
); );
alter table projects rename constraint projects_client_id_fkey to client;
-- --
-- Name: projects_view; Type: VIEW; Schema: test; Owner: - -- Name: projects_view; Type: VIEW; Schema: test; Owner: -
@@ -695,25 +625,23 @@ CREATE TABLE simple_pk (
extra character varying NOT NULL extra character varying NOT NULL
); );
-- CREATE TABLE users (
-- Name: users_projects; Type: TABLE; Schema: test; Owner: - id integer primary key,
-- name text NOT NULL
);
CREATE TABLE users_projects ( CREATE TABLE users_projects (
user_id integer NOT NULL, user_id integer NOT NULL REFERENCES users(id),
project_id integer NOT NULL project_id integer NOT NULL REFERENCES projects(id),
PRIMARY KEY (project_id, user_id)
); );
--
-- Name: tasks; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE tasks ( CREATE TABLE tasks (
id integer NOT NULL, id integer primary key,
name text NOT NULL, name text NOT NULL,
project_id integer project_id integer REFERENCES projects(id)
); );
alter table tasks rename constraint tasks_project_id_fkey to project;
CREATE OR REPLACE VIEW filtered_tasks AS CREATE OR REPLACE VIEW filtered_tasks AS
SELECT id AS "myId", name, project_id AS "projectID" SELECT id AS "myId", name, project_id AS "projectID"
@@ -725,6 +653,53 @@ project_id IN (
SELECT project_id FROM users_projects WHERE user_id = 1 SELECT project_id FROM users_projects WHERE user_id = 1
); );
CREATE TABLE users_tasks (
user_id integer NOT NULL REFERENCES users(id),
task_id integer NOT NULL REFERENCES tasks(id),
primary key (task_id, user_id)
);
CREATE TABLE comments (
id integer primary key,
commenter_id integer NOT NULL,
user_id integer NOT NULL,
task_id integer NOT NULL,
content text NOT NULL
);
alter table only comments
add constraint "user" foreign key (commenter_id) references users(id),
add constraint comments_task_id_fkey foreign key (task_id, user_id) references users_tasks(task_id, user_id);
create table private.articles (
id integer primary key,
body text,
owner name not null
);
create table private.article_stars (
article_id integer not null,
user_id integer not null,
created_at timestamp without time zone default now() not null,
primary key (article_id, user_id)
);
alter table only private.article_stars
add constraint article foreign key (article_id) references private.articles(id),
add constraint "user" foreign key (user_id) references test.users(id);
CREATE VIEW limited_article_stars AS
SELECT article_id, user_id, created_at FROM private.article_stars;
CREATE VIEW "articleStars" AS
SELECT article_stars.article_id AS "articleId",
article_stars.user_id AS "userId",
article_stars.created_at AS "createdAt"
FROM private.article_stars;
CREATE VIEW articles AS
SELECT articles.id,
articles.body,
articles.owner
FROM private.articles;
-- --
-- Name: tsearch; Type: TABLE; Schema: test; Owner: - -- Name: tsearch; Type: TABLE; Schema: test; Owner: -
@@ -734,28 +709,6 @@ CREATE TABLE tsearch (
text_search_vector tsvector text_search_vector tsvector
); );
--
-- Name: users; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE users (
id integer NOT NULL,
name text NOT NULL
);
--
-- Name: users_tasks; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE users_tasks (
user_id integer NOT NULL,
task_id integer NOT NULL
);
CREATE TABLE "Escap3e;" ( CREATE TABLE "Escap3e;" (
"so6meIdColumn" integer primary key "so6meIdColumn" integer primary key
); );
@@ -773,7 +726,6 @@ CREATE TABLE clashing_column (
t text t text
); );
-- --
-- Name: id; Type: DEFAULT; Schema: test; Owner: - -- Name: id; Type: DEFAULT; Schema: test; Owner: -
-- --
@@ -788,13 +740,6 @@ ALTER TABLE ONLY auto_incrementing_pk ALTER COLUMN id SET DEFAULT nextval('auto_
ALTER TABLE ONLY has_fk ALTER COLUMN id SET DEFAULT nextval('has_fk_id_seq'::regclass); ALTER TABLE ONLY has_fk ALTER COLUMN id SET DEFAULT nextval('has_fk_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: test; Owner: -
--
ALTER TABLE ONLY items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regclass);
SET search_path = postgrest, pg_catalog; SET search_path = postgrest, pg_catalog;
-- --
@@ -805,24 +750,6 @@ ALTER TABLE ONLY auth
ADD CONSTRAINT auth_pkey PRIMARY KEY (id); ADD CONSTRAINT auth_pkey PRIMARY KEY (id);
SET search_path = private, pg_catalog;
--
-- Name: articles_pkey; Type: CONSTRAINT; Schema: private; Owner: -
--
ALTER TABLE ONLY articles
ADD CONSTRAINT articles_pkey PRIMARY KEY (id);
--
-- Name: user_article; Type: CONSTRAINT; Schema: private; Owner: -
--
ALTER TABLE ONLY article_stars
ADD CONSTRAINT user_article PRIMARY KEY (article_id, user_id);
SET search_path = test, pg_catalog; SET search_path = test, pg_catalog;
-- --
@@ -840,23 +767,6 @@ ALTER TABLE ONLY authors_only
ALTER TABLE ONLY auto_incrementing_pk ALTER TABLE ONLY auto_incrementing_pk
ADD CONSTRAINT auto_incrementing_pk_pkey PRIMARY KEY (id); ADD CONSTRAINT auto_incrementing_pk_pkey PRIMARY KEY (id);
--
-- Name: clients_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY clients
ADD CONSTRAINT clients_pkey PRIMARY KEY (id);
--
-- Name: comments_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY comments
ADD CONSTRAINT comments_pkey PRIMARY KEY (id);
-- --
-- Name: complex_items_pkey; Type: CONSTRAINT; Schema: test; Owner: - -- Name: complex_items_pkey; Type: CONSTRAINT; Schema: test; Owner: -
-- --
@@ -888,15 +798,6 @@ ALTER TABLE ONLY simple_pk
ALTER TABLE ONLY has_fk ALTER TABLE ONLY has_fk
ADD CONSTRAINT has_fk_pkey PRIMARY KEY (id); ADD CONSTRAINT has_fk_pkey PRIMARY KEY (id);
--
-- Name: items_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY items
ADD CONSTRAINT items_pkey PRIMARY KEY (id);
-- --
-- Name: menagerie_pkey; Type: CONSTRAINT; Schema: test; Owner: - -- Name: menagerie_pkey; Type: CONSTRAINT; Schema: test; Owner: -
-- --
@@ -905,46 +806,6 @@ ALTER TABLE ONLY menagerie
ADD CONSTRAINT menagerie_pkey PRIMARY KEY ("integer"); ADD CONSTRAINT menagerie_pkey PRIMARY KEY ("integer");
--
-- Name: project_user; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_projects
ADD CONSTRAINT project_user PRIMARY KEY (project_id, user_id);
--
-- Name: projects_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY projects
ADD CONSTRAINT projects_pkey PRIMARY KEY (id);
--
-- Name: task_user; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_tasks
ADD CONSTRAINT task_user PRIMARY KEY (task_id, user_id);
--
-- Name: tasks_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY tasks
ADD CONSTRAINT tasks_pkey PRIMARY KEY (id);
--
-- Name: users_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
SET search_path = postgrest, pg_catalog; SET search_path = postgrest, pg_catalog;
-- --
@@ -971,43 +832,8 @@ SET search_path = test, pg_catalog;
CREATE TRIGGER secrets_owner_track BEFORE INSERT OR UPDATE ON authors_only FOR EACH ROW EXECUTE PROCEDURE postgrest.set_authors_only_owner(); CREATE TRIGGER secrets_owner_track BEFORE INSERT OR UPDATE ON authors_only FOR EACH ROW EXECUTE PROCEDURE postgrest.set_authors_only_owner();
SET search_path = private, pg_catalog;
--
-- Name: article_stars_article_id_fkey; Type: FK CONSTRAINT; Schema: private; Owner: -
--
ALTER TABLE ONLY article_stars
ADD CONSTRAINT article_stars_article_id_fkey FOREIGN KEY (article_id) REFERENCES articles(id);
--
-- Name: article_stars_user_id_fkey; Type: FK CONSTRAINT; Schema: private; Owner: -
--
ALTER TABLE ONLY article_stars
ADD CONSTRAINT article_stars_user_id_fkey FOREIGN KEY (user_id) REFERENCES test.users(id);
SET search_path = test, pg_catalog; SET search_path = test, pg_catalog;
--
-- Name: comments_commenter_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY comments
ADD CONSTRAINT comments_commenter_id_fkey FOREIGN KEY (commenter_id) REFERENCES users(id);
--
-- Name: comments_task_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY comments
ADD CONSTRAINT comments_task_id_fkey FOREIGN KEY (task_id, user_id) REFERENCES users_tasks(task_id, user_id);
-- --
-- Name: has_fk_fk_fkey; Type: FK CONSTRAINT; Schema: test; Owner: - -- Name: has_fk_fk_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
-- --
@@ -1023,55 +849,6 @@ ALTER TABLE ONLY has_fk
ALTER TABLE ONLY has_fk ALTER TABLE ONLY has_fk
ADD CONSTRAINT has_fk_simple_fk_fkey FOREIGN KEY (simple_fk) REFERENCES simple_pk(k); ADD CONSTRAINT has_fk_simple_fk_fkey FOREIGN KEY (simple_fk) REFERENCES simple_pk(k);
--
-- Name: projects_client_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY projects
ADD CONSTRAINT projects_client_id_fkey FOREIGN KEY (client_id) REFERENCES clients(id);
--
-- Name: tasks_project_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY tasks
ADD CONSTRAINT tasks_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id);
--
-- Name: users_projects_project_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_projects
ADD CONSTRAINT users_projects_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id);
--
-- Name: users_projects_user_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_projects
ADD CONSTRAINT users_projects_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
--
-- Name: users_tasks_task_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_tasks
ADD CONSTRAINT users_tasks_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id);
--
-- Name: users_tasks_user_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_tasks
ADD CONSTRAINT users_tasks_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
create table addresses ( create table addresses (
id int not null unique, id int not null unique,
address text not null address text not null
@@ -1083,6 +860,8 @@ create table orders (
billing_address_id int references addresses(id), billing_address_id int references addresses(id),
shipping_address_id int references addresses(id) shipping_address_id int references addresses(id)
); );
alter table orders rename constraint orders_billing_address_id_fkey to billing;
alter table orders rename constraint orders_shipping_address_id_fkey to shipping;
CREATE FUNCTION getproject(id int) RETURNS SETOF projects CREATE FUNCTION getproject(id int) RETURNS SETOF projects
LANGUAGE sql LANGUAGE sql
@@ -1096,11 +875,17 @@ CREATE FUNCTION get_projects_below(id int) RETURNS SETOF projects
SELECT * FROM test.projects WHERE id < $1; SELECT * FROM test.projects WHERE id < $1;
$_$; $_$;
CREATE FUNCTION get_projects_above(id int) RETURNS SETOF projects
LANGUAGE sql
AS $_$
SELECT * FROM test.projects WHERE id > $1;
$_$ ROWS 1;
CREATE FUNCTION getallprojects() RETURNS SETOF projects CREATE FUNCTION getallprojects() RETURNS SETOF projects
LANGUAGE sql LANGUAGE sql
AS $_$ AS $_$
SELECT * FROM test.projects; SELECT * FROM test.projects;
$_$; $_$ ROWS 2019;
CREATE FUNCTION setprojects(id_l int, id_h int, name text) RETURNS SETOF projects CREATE FUNCTION setprojects(id_l int, id_h int, name text) RETURNS SETOF projects
LANGUAGE sql LANGUAGE sql
@@ -1373,6 +1158,18 @@ create table test.tiobe_pls(
rank smallint rank smallint
); );
create table test.single_unique(
unique_key integer unique not null,
value text
);
create table test.compound_unique(
key1 integer not null,
key2 integer not null,
value text,
unique(key1, key2)
);
create table test.family_tree ( create table test.family_tree (
id text not null primary key, id text not null primary key,
name text not null, name text not null,
@@ -1388,12 +1185,11 @@ create table test.managers (
create table test.organizations ( create table test.organizations (
id integer primary key, id integer primary key,
name text, name text,
referee integer, referee integer references organizations(id),
auditor integer, auditor integer references organizations(id),
manager_id integer references managers(id) manager_id integer references managers(id)
); );
alter table only test.organizations add constraint pptr1 foreign key (referee) references test.organizations(id); alter table only test.organizations rename constraint organizations_manager_id_fkey to manager;
alter table only test.organizations add constraint pptr2 foreign key (auditor) references test.organizations(id);
create table private.authors( create table private.authors(
id integer primary key, id integer primary key,
@@ -1730,3 +1526,232 @@ select $$
</html> </html>
$$::text; $$::text;
$_$ language sql; $_$ language sql;
create view getallprojects_view as
select * from getallprojects();
create view get_projects_above_view as
select * from get_projects_above(1);
CREATE TABLE web_content (
id integer,
name text,
p_web_id integer references web_content(id),
primary key (id)
);
CREATE FUNCTION getallusers() RETURNS SETOF users AS $$
SELECT * FROM test.users;
$$ LANGUAGE sql;
create table app_users (
id integer primary key,
email text unique not null,
password text not null
);
create table private.pages (
link int not null unique
, url text
);
create table private.referrals (
site text
, link int references private.pages(link) not null
);
create view test.pages as select * from private.pages;
create view test.referrals as select * from private.referrals;
create table big_projects (
big_project_id serial primary key,
name text
);
create table sites (
site_id serial primary key
, name text
, main_project_id int null references big_projects (big_project_id)
);
alter table sites rename constraint sites_main_project_id_fkey to main_project;
create table jobs (
job_id uuid primary key
, name text
, site_id int not null references sites (site_id)
, big_project_id int not null references big_projects (big_project_id)
);
create view main_jobs as
select * from jobs
where site_id in (select site_id from sites where main_project_id is not null);
-- junction in a private schema, just to make sure we don't leak it on resource embedding
-- if it leaks it would show on the disambiguation error tests
create view private.priv_jobs as
select * from jobs;
-- tables to show our limitation when trying to do an m2m embed
-- with a junction table that has more than two foreign keys
create table whatev_projects (
id serial primary key,
name text
);
create table whatev_sites (
id serial primary key
, name text
);
create table whatev_jobs (
job_id uuid primary key
, name text
, site_id_1 int not null references whatev_sites (id)
, project_id_1 int not null references whatev_projects (id)
, site_id_2 int not null references whatev_sites (id)
, project_id_2 int not null references whatev_projects (id)
);
-- circular reference
create table agents (
id int primary key
, name text
, department_id int
);
create table departments (
id int primary key
, name text
, head_id int references agents(id)
);
ALTER TABLE agents
ADD CONSTRAINT agents_department_id_fkey foreign key (department_id) REFERENCES departments(id);
-- composite key disambiguation
create table schedules (
id int primary key
, name text
, start_at timetz
, end_at timetz
);
create table activities (
id int
, schedule_id int
, car_id text
, camera_id text
, primary key (id, schedule_id)
);
alter table activities
add constraint schedule foreign key (schedule_id)
references schedules (id);
create table unit_workdays (
unit_id int
, day date
, fst_shift_activity_id int
, fst_shift_schedule_id int
, snd_shift_activity_id int
, snd_shift_schedule_id int
, primary key (unit_id, day)
);
alter table unit_workdays
add constraint fst_shift foreign key (fst_shift_activity_id, fst_shift_schedule_id)
references activities (id, schedule_id),
add constraint snd_shift foreign key (snd_shift_activity_id, snd_shift_schedule_id)
references activities (id, schedule_id);
-- for a pre-request function
create or replace function custom_headers() returns void as $$
declare
user_agent text := current_setting('request.header.user-agent', true);
req_path text := current_setting('request.path', true);
req_accept text := current_setting('request.header.accept', true);
begin
if user_agent similar to 'MSIE (6.0|7.0)' then
perform set_config('response.headers',
'[{"Cache-Control": "no-cache, no-store, must-revalidate"}]', false);
elsif req_path similar to '/(items|projects)' and req_accept = 'text/csv' then
perform set_config('response.headers',
format('[{"Content-Disposition": "attachment; filename=%s.csv"}]', trim('/' from req_path)), false);
elsif req_path similar to '/(clients|rpc/getallprojects)' then
perform set_config('response.headers',
'[{"Content-Type": "application/geo+json"}]', false);
else
perform set_config('response.headers',
'[{"X-Custom-Header": "mykey=myval"}]', false);
end if;
end; $$ language plpgsql;
create table private.stuff(
id integer primary key
, name text
);
create view test.stuff as select * from private.stuff;
create or replace function location_for_stuff() returns trigger
as $$
begin
insert into private.stuff values (new.id, new.name);
if new.id is not null
then
perform set_config(
'response.headers'
, format('[{"Location": "/%s?id=eq.%s&overriden=true"}]', tg_table_name, new.id)
, true
);
end if;
return new;
end
$$ language plpgsql security definer;
create trigger location_for_stuff instead of insert on test.stuff for each row execute procedure test.location_for_stuff();
create table loc_test (
id int primary key
, c text
);
-- tables to test multi schema access in one instance
create table v1.parents (
id int primary key
, name text
);
create table v1.children (
id serial primary key
, name text
, parent_id int
, constraint parent foreign key(parent_id)
references v1.parents(id)
);
create function v1.get_parents_below(id int)
returns setof v1.parents as $$
select * from v1.parents where id < $1;
$$ language sql;
create table v2.parents (
id int primary key
, name text
);
create table v2.children (
id serial primary key
, name text
, parent_id int
, constraint parent foreign key(parent_id)
references v2.parents(id)
);
create table v2.another_table (
id int primary key
, another_value text
);
create function v2.get_parents_below(id int)
returns setof v2.parents as $$
select * from v2.parents where id < $1;
$$ language sql;
+12 -12
View File
@@ -94,21 +94,21 @@ setUp
echo "Running memory usage tests.." echo "Running memory usage tests.."
jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "12M" jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "13M"
jsonKeyTest "1M" "POST" "/leak?columns=blob" "12M" jsonKeyTest "1M" "POST" "/leak?columns=blob" "13M"
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "12M" jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "13M"
jsonKeyTest "10M" "POST" "/rpc/leak?columns=blob" "40M" jsonKeyTest "10M" "POST" "/rpc/leak?columns=blob" "41M"
jsonKeyTest "10M" "POST" "/leak?columns=blob" "40M" jsonKeyTest "10M" "POST" "/leak?columns=blob" "41M"
jsonKeyTest "10M" "PATCH" "/leak?id=eq.1&columns=blob" "40M" jsonKeyTest "10M" "PATCH" "/leak?id=eq.1&columns=blob" "41M"
jsonKeyTest "50M" "POST" "/rpc/leak?columns=blob" "170M" jsonKeyTest "50M" "POST" "/rpc/leak?columns=blob" "171M"
jsonKeyTest "50M" "POST" "/leak?columns=blob" "170M" jsonKeyTest "50M" "POST" "/leak?columns=blob" "171M"
jsonKeyTest "50M" "PATCH" "/leak?id=eq.1&columns=blob" "170M" jsonKeyTest "50M" "PATCH" "/leak?id=eq.1&columns=blob" "171M"
postJsonArrayTest "1000" "/perf_articles?columns=id,body" "10M" postJsonArrayTest "1000" "/perf_articles?columns=id,body" "11M"
postJsonArrayTest "10000" "/perf_articles?columns=id,body" "10M" postJsonArrayTest "10000" "/perf_articles?columns=id,body" "11M"
postJsonArrayTest "100000" "/perf_articles?columns=id,body" "20M" postJsonArrayTest "100000" "/perf_articles?columns=id,body" "21M"
cleanUp cleanUp

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