Compare commits

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

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

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

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

- Previously: `/json_arr?select=data->>2::json`
- Now: `/json_arr?select=data->2`
2018-06-19 11:17:59 -05:00
steve-chavez 30dfadec7b Add support for getting json by array index
* Also support json negative array index
2018-06-19 11:17:59 -05:00
steve-chavez 2513c00039 Refactor Field type,move tests to JsonOperatorSpec 2018-06-19 11:17:59 -05:00
steve-chavez 100bf494ac Move database creation/destruction scripts to sh
Bash is not available on /bin/bash on some linux distros like NixOS
2018-06-19 11:17:59 -05:00
steve-chavez e8188b0d41 Add Lead Backer. Thank you Daniel. 2018-06-18 11:49:15 -05:00
steve-chavez 3958ebbb05 Add CHANGELOG note about stored procedures 2018-05-31 11:57:42 -05:00
steve-chavez 37e7398a85 Fix UPSERT bug on camelCase PK column 2018-05-31 11:57:42 -05:00
Felix YanandSteve Chávez 9ea7529f30 Use protolude 0.2.2, replace undefined with witness
protolude provides headMay since 0.2.2, it can be used instead of safe.
2018-05-28 13:08:27 -05:00
ilovezfsandSteve Chávez 384767708b postgrest.cabal: fix constraints on protolude and hasql-transaction 2018-05-27 08:54:46 -05:00
steve-chavez 6bcbb124d2 Add PGRST_ROLE_CLAIM_KEY to ENV 2018-05-25 10:27:09 -05:00
DominikandSteve Chávez f6c1ff810e Add role-claim-key to docker postgrest.conf
With #1091, the role-claim-key was added, but it is not configurable in docker containers as the template config doesn't allow it.
This patch adds the necessary line to the postgrest.conf of the docker container.
2018-05-25 09:26:44 -05:00
steve-chavez f80cfbf165 Add BACKERS.md 2018-05-21 13:11:24 -05:00
steve-chavez d8896be2c1 Add more details to issue template 2018-05-16 09:25:24 -05:00
60 changed files with 1620 additions and 634 deletions
+46 -6
View File
@@ -52,7 +52,7 @@ build-distro-bin: &build-distro-bin
key: v1-{{ .Environment.CIRCLE_JOB }}-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
jobs:
build-test:
build-test-9.4:
docker:
- image: circleci/buildpack-deps:trusty
environment:
@@ -138,6 +138,38 @@ jobs:
name: run tests
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test
build-test-10:
docker:
- image: circleci/buildpack-deps:trusty
environment:
- PGHOST=localhost
- image: circleci/postgres:10.5
environment:
- POSTGRES_USER=circleci
- POSTGRES_DB=circleci
steps:
- checkout
- restore_cache:
keys:
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
- run:
name: install stack & dependencies
command: |
curl -L https://github.com/commercialhaskell/stack/releases/download/v1.1.2/stack-1.1.2-linux-x86_64.tar.gz | tar zx -C /tmp
sudo mv /tmp/stack-1.1.2-linux-x86_64/stack /usr/bin
sudo apt-get update
sudo apt-get install -y libgmp-dev
sudo apt-get install -y postgresql-client
stack setup
- run:
name: build src and tests
command: |
stack build --fast -j1
stack build --fast --test --no-run-tests
- run:
name: run tests
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test
build-prof-test:
docker:
- image: circleci/buildpack-deps:trusty
@@ -224,7 +256,7 @@ workflows:
version: 2
build-test-release:
jobs:
- build-test:
- build-test-9.4:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
@@ -232,14 +264,19 @@ workflows:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
- build-test-10:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
- build-prof-test:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
- centos6:
requires:
- build-test
- build-test-9.4
- build-test-9.6
- build-test-10
- build-prof-test
filters:
tags:
@@ -248,8 +285,9 @@ workflows:
ignore: /.*/
- centos7:
requires:
- build-test
- build-test-9.4
- build-test-9.6
- build-test-10
- build-prof-test
filters:
tags:
@@ -258,8 +296,9 @@ workflows:
ignore: /.*/
- ubuntu:
requires:
- build-test
- build-test-9.4
- build-test-9.6
- build-test-10
- build-prof-test
filters:
tags:
@@ -268,8 +307,9 @@ workflows:
ignore: /.*/
- ubuntui386:
requires:
- build-test
- build-test-9.4
- build-test-9.6
- build-test-10
- build-prof-test
filters:
tags:
+5 -5
View File
@@ -1,11 +1,11 @@
### Environment
* Version of postgrest:
* PostgreSQL version: (if using docker, specify the image)
* PostgREST version: (if using docker, specify the image)
* Operating system:
* Running in docker:
* [ ] yes
* [ ] no
### Description of issue
(Expected behavior vs actual behavior.)
(Expected behavior vs actual behavior)
(Steps to reproduce: Include a minimal SQL definition plus how you make the request to PostgREST and the response body)
+14
View File
@@ -0,0 +1,14 @@
# Sponsors & Backers
PostgREST ongoing development is only possible thanks to our Sponsors and Backers, listed below. If you'd like to join them, you can do so by supporting the PostgREST organization on [Patreon](https://www.patreon.com/postgrest).
## Lead Backers
- [Christiaan Westerbeek](https://devotis.nl)
- [Daniel Babiak](https://github.com/d-babiak)
- [Michel Pelletier](https://github.com/michelp/)
## Backers
- Tsingson Qin
- Jay Hannah
+49 -1
View File
@@ -9,8 +9,54 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
## [5.2.0] - 2018-12-12
### Added
- #1205, Add support for parsing JSON Web Key Sets - @russelldavies
- #1203, Add support for reading db-uri from a separate file - @zhoufeng1989
- #1200, Add db-extra-search-path config for adding schemas to the search_path, solves issues related to extensions created on the public schema - @steve-chavez
- #1219, Add ability to quote column names on filters - @steve-chavez
### Fixed
- #1182, Fix embedding on views with composite pks - @steve-chavez
- #1180, Fix embedding on views with subselects in pg10 - @steve-chavez
- #1197, Allow CORS for PUT - @bkylerussell
- #1181, Correctly qualify function argument of custom type in public schema - @steve-chavez
- #1008, Allow columns that contain spaces in filters - @steve-chavez
## [5.1.0] - 2018-08-31
### Added
- #1099, Add support for getting json/jsonb by array index - @steve-chavez
- #1145, Add materialized view columns to OpenAPI output - @steve-chavez
- #709, Allow embedding on views with subselects/CTE - @steve-chavez
- #1148, OpenAPI: add `required` section for the non-nullable columns - @laughedelic
- #1158, Add summary to OpenAPI doc for RPC functions - @mdr1384
### Fixed
- #1113, Fix UPSERT failing when having a camel case PK column - @steve-chavez
- #945, Fix slow start-up time on big schemas - @steve-chavez
- #1129, Fix view embedding when table is capitalized - @steve-chavez
- #1149, OpenAPI: Change `GET` response type to array - @laughedelic
- #1152, Fix RPC failing when having arguments with reserved or uppercase keywords - @mdr1384
- #905, Fix intermittent empty replies - @steve-chavez
- #1139, Fix JWTIssuedAtFuture failure for valid iat claim - @steve-chavez
- #1141, Fix app.settings resetting on pool timeout - @steve-chavez
### Changed
- #1099, Numbers in json path `?select=data->1->>key` now get treated as json array indexes instead of keys - @steve-chavez
- #1128, Allow finishing a json path with a single arrow `->`. Now a json can be obtained without resorting to casting, Previously: `/json_arr?select=data->>2::json`, now: `/json_arr?select=data->2` - @steve-chavez
- #724, Change server-host default of *4 to 127.0.0.1
### Deprecated
- #724, SIGHUP deprecated, SIGUSR1 should be used instead
## [0.5.0.0] - 2018-05-14
### Added
@@ -32,7 +78,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Changed
- Computed columns now only work if they belong to the db-schema - @steve-chavez
- #828, A `SET SCHEMA <db-schema>` is done on each request, this has the following implications:
- Computed columns now only work if they belong to the db-schema
- Stored procedures might require a `search_path` to work properly, for further details see https://postgrest.org/en/v5.0/api.html#explicit-qualification
- To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez
- Overloaded functions now depend on the `dbStructure`, restart/sighup may be needed for their correct functioning - @steve-chavez
- #1098, Removed support for:
+26 -14
View File
@@ -1,19 +1,21 @@
![Logo](static/logo.png "Logo")
[![Build Status](https://circleci.com/gh/PostgREST/postgrest/tree/master.svg?style=shield)](https://circleci.com/gh/PostgREST/postgrest/tree/master)
[![Donate](https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854)](https://www.patreon.com/postgrest)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/postgrest)
<a href="https://heroku.com/deploy?template=https://github.com/PostgREST/postgrest">
<img src="https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg" alt="Deploy">
</a>
[![Join the chat at https://gitter.im/begriffs/postgrest](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/begriffs/postgrest)
[![Docs](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](http://postgrest.org)
[![Docker Stars](https://img.shields.io/docker/pulls/postgrest/postgrest.svg)](https://hub.docker.com/r/postgrest/postgrest/)
[![Donate](https://img.shields.io/badge/patreon-donate-orange.svg?colorB=F96854)](https://www.patreon.com/postgrest)
[![Build Status](https://circleci.com/gh/PostgREST/postgrest/tree/master.svg?style=shield)](https://circleci.com/gh/PostgREST/postgrest/tree/master)
[![Hackage docs](https://img.shields.io/hackage/v/postgrest.svg?label=hackage)](http://hackage.haskell.org/package/postgrest)
PostgREST serves a fully RESTful API from any existing PostgreSQL
database. It provides a cleaner, more standards-compliant, faster
API than you are likely to write from scratch.
### Usage
## Usage
1. Download the binary ([latest release](https://github.com/PostgREST/postgrest/releases/latest))
for your platform.
@@ -23,7 +25,7 @@ API than you are likely to write from scratch.
postgrest --help
```
### Performance
## Performance
TLDR; subsecond response times for up to 2000 requests/sec on Heroku
free tier. If you're used to servers written in interpreted languages
@@ -51,7 +53,7 @@ by
* Using the PostgreSQL binary protocol
* Being stateless to allow horizontal scaling
### Security
## Security
PostgREST [handles
authentication](http://postgrest.org/en/stable/auth.html) (via JSON Web
@@ -72,7 +74,7 @@ are limited to certain templates using
functions, the trigger workaround does not compromise row-level
security.
### Versioning
## Versioning
A robust long-lived API needs the freedom to exist in multiple
versions. PostgREST does versioning through database schemas. This
@@ -80,7 +82,7 @@ allows you to expose tables and views without making the app brittle.
Underlying tables can be superseded and hidden behind public facing
views.
### Self-documentation
## Self-documentation
PostgREST uses the [OpenAPI](https://openapis.org/) standard to
generate up-to-date documentation for APIs. You can use a tool like
@@ -92,7 +94,7 @@ instance the number of rows returned by an endpoint is reported by -
and limited with - range headers. More about
[that](http://begriffs.com/posts/2014-03-06-beyond-http-header-links.html).
### Data Integrity
## Data Integrity
Rather than relying on an Object Relational Mapper and custom
imperative coding, this system requires you put declarative constraints
@@ -106,12 +108,22 @@ See examples of [PostgreSQL
constraints](http://www.tutorialspoint.com/postgresql/postgresql_constraints.htm)
and the [API guide](http://postgrest.org/en/stable/api.html).
### Thanks
## Supporting development
The PostgREST organization is grateful to the generous project
[contributors](https://github.com/PostgREST/postgrest/graphs/contributors)
who have improved PostgREST immensely with their code and good
judgement. See more details in the
[changelog](https://github.com/PostgREST/postgrest/blob/master/CHANGELOG.md).
You can help PostgREST ongoing maintenance and development by:
- Making a regular donation through Patreon https://www.patreon.com/postgrest
- Alternatively, you can make a one-time donation via Paypal https://www.paypal.me/postgrest
Every donation will be spent on making PostgREST better for the whole community.
## Thanks
The PostgREST organization is grateful to:
- The project [sponsors and backers](https://github.com/PostgREST/postgrest/blob/master/BACKERS.md) who support PostgREST's development.
- The project [contributors](https://github.com/PostgREST/postgrest/graphs/contributors) who have improved PostgREST immensely with their code
and good judgement. See more details in the [changelog](https://github.com/PostgREST/postgrest/blob/master/CHANGELOG.md).
The cool logo came from [Mikey Casalaina](https://github.com/casalaina).
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "PostgREST",
"description": "RESTful API for any PostgreSQL database.",
"logo": "https://halcyon.sh/logo.svg",
"logo": "https://avatars2.githubusercontent.com/u/15115011",
"repository": "https://github.com/PostgREST/postgrest",
"env": {
"BUILDPACK_URL": {
@@ -10,10 +10,10 @@
},
"POSTGREST_VER": {
"description": "Version of PostgREST to deploy",
"value": "0.5.0.0"
"value": "5.2.0"
},
"DB_URI": {
"description": "Database connection string",
"description": "Database connection string, e.g. postgres://user:pass@xxxxxxx.rds.amazonaws.com/mydb",
"required": true
},
"DB_SCHEMA": {
+2 -1
View File
@@ -35,7 +35,8 @@ ENV PGRST_DB_URI= \
PGRST_SECRET_IS_BASE64=false \
PGRST_JWT_AUD= \
PGRST_MAX_ROWS= \
PGRST_PRE_REQUEST=
PGRST_PRE_REQUEST= \
PGRST_ROLE_CLAIM_KEY=".role"
# PostgREST reads /etc/postgrest.conf so map the configuration
# file in when you run this container
+1
View File
@@ -10,6 +10,7 @@ server-proxy-uri = "$(PGRST_SERVER_PROXY_URI)"
jwt-secret = "$(PGRST_JWT_SECRET)"
secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)"
jwt-aud = "$(PGRST_JWT_AUD)"
role-claim-key = "$(PGRST_ROLE_CLAIM_KEY)"
max-rows = "$(PGRST_MAX_ROWS)"
pre-request = "$(PGRST_PRE_REQUEST)"
+58 -50
View File
@@ -3,40 +3,38 @@
module Main where
import PostgREST.App (postgrest)
import PostgREST.Config (AppConfig (..),
minimumPgVersion,
prettyVersion, readOptions)
import PostgREST.DbStructure (getDbStructure, getPgVersion,
fillSessionWithSettings)
import PostgREST.Error (encodeError)
import PostgREST.OpenAPI (isMalformedProxyUri)
import PostgREST.Types (DbStructure, Schema, PgVersion(..))
import Protolude hiding (hPutStrLn, replace)
import PostgREST.App (postgrest)
import PostgREST.Config (AppConfig (..),
prettyVersion, readOptions)
import PostgREST.DbStructure (getDbStructure, getPgVersion)
import PostgREST.Error (encodeError)
import PostgREST.OpenAPI (isMalformedProxyUri)
import PostgREST.Types (DbStructure, Schema, PgVersion(..), minimumPgVersion)
import Protolude hiding (hPutStrLn, replace)
import Control.AutoUpdate (defaultUpdateSettings,
mkAutoUpdate, updateAction)
import Control.Retry (RetryStatus, capDelay,
exponentialBackoff,
retrying, rsPreviousDelay)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.IORef (IORef, atomicWriteIORef,
newIORef, readIORef)
import Data.String (IsString (..))
import Data.Text (pack, replace, stripPrefix, strip)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Text.IO (hPutStrLn)
import Data.Time.Clock (getCurrentTime)
import qualified Hasql.Pool as P
import qualified Hasql.Session as H
import Network.Wai.Handler.Warp (defaultSettings,
runSettings, setHost,
setPort, setServerName,
setTimeout)
import System.IO (BufferMode (..),
hSetBuffering)
import Control.AutoUpdate (defaultUpdateSettings,
mkAutoUpdate, updateAction)
import Control.Retry (RetryStatus, capDelay,
exponentialBackoff,
retrying, rsPreviousDelay)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.IORef (IORef, atomicWriteIORef,
newIORef, readIORef)
import Data.String (IsString (..))
import Data.Text (pack, replace, stripPrefix, strip)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Text.IO (hPutStrLn, readFile)
import Data.Time.Clock (getCurrentTime)
import qualified Hasql.Pool as P
import qualified Hasql.Session as H
import qualified Hasql.Transaction.Sessions as HT
import Network.Wai.Handler.Warp (defaultSettings,
runSettings, setHost,
setPort, setServerName)
import System.IO (BufferMode (..),
hSetBuffering)
#ifndef mingw32_HOST_OS
import System.Posix.Signals
@@ -64,11 +62,10 @@ connectionWorker
:: ThreadId -- ^ This thread is killed if pg version is unsupported
-> P.Pool -- ^ The PostgreSQL connection pool
-> Schema -- ^ Schema PostgREST is serving up
-> [(Text, Text)] -- ^ Settings or Environment passed in through the config
-> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure'
-> IORef Bool -- ^ Used as a binary Semaphore
-> IO ()
connectionWorker mainTid pool schema settings refDbStructure refIsWorkerOn = do
connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do
isWorkerOn <- readIORef refIsWorkerOn
unless isWorkerOn $ do
atomicWriteIORef refIsWorkerOn True
@@ -86,8 +83,7 @@ connectionWorker mainTid pool schema settings refDbStructure refIsWorkerOn = do
("Cannot run in this PostgreSQL version, PostgREST needs at least "
<> pgvName minimumPgVersion)
killThread mainTid
fillSessionWithSettings settings
dbStructure <- getDbStructure schema actualPgVersion
dbStructure <- HT.transaction HT.ReadCommitted HT.Read $ getDbStructure schema actualPgVersion
liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure
case result of
Left e -> do
@@ -143,7 +139,7 @@ main = do
--
-- readOptions builds the 'AppConfig' from the config file specified on the
-- command line
conf <- loadSecretFile =<< readOptions
conf <- loadDbUriFile =<< loadSecretFile =<< readOptions
let host = configHost conf
port = configPort conf
proxy = configProxyUri conf
@@ -152,8 +148,7 @@ main = do
appSettings =
setHost ((fromString . toS) host) -- Warp settings
. setPort port
. setServerName (toS $ "postgrest/" <> prettyVersion)
. setTimeout 3600 $
. setServerName (toS $ "postgrest/" <> prettyVersion) $
defaultSettings
-- Checks that the provided proxy uri is formated correctly
@@ -186,7 +181,6 @@ main = do
mainTid
pool
(configSchema conf)
(configSettings conf)
refDbStructure
refIsWorkerOn
--
@@ -204,15 +198,15 @@ main = do
throwTo mainTid UserInterrupt
) Nothing
void $ installHandler sigHUP (
Catch $ connectionWorker
mainTid
pool
(configSchema conf)
(configSettings conf)
refDbStructure
refIsWorkerOn
) Nothing
forM_ [sigHUP, sigUSR1] $ \sig ->
void $ installHandler sig (
Catch $ connectionWorker
mainTid
pool
(configSchema conf)
refDbStructure
refIsWorkerOn
) Nothing
#endif
@@ -230,7 +224,6 @@ main = do
mainTid
pool
(configSchema conf)
(configSettings conf)
refDbStructure
refIsWorkerOn)
@@ -285,3 +278,18 @@ loadSecretFile conf = extractAndTransform mSecret
-- replace: Replace every occurrence of one substring with another
replaceUrlChars =
replace "_" "/" . replace "-" "+" . replace "." "="
{-
Load database uri from a separate file if `db-uri` is a filepath.
-}
loadDbUriFile :: AppConfig -> IO AppConfig
loadDbUriFile conf = extractDbUri mDbUri
where
mDbUri = configDatabase conf
extractDbUri :: Text -> IO AppConfig
extractDbUri dbUri =
fmap setDbUri $
case stripPrefix "@" dbUri of
Nothing -> return dbUri
Just filename -> strip <$> readFile (toS filename)
setDbUri dbUri = conf {configDatabase = dbUri}
+25 -19
View File
@@ -2,14 +2,16 @@ name: postgrest
description: Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security
permits.
version: 0.5.0.0
version: 5.2.0
synopsis: REST API for any Postgres database
license: MIT
license-file: LICENSE
author: Joe Nelson, Adam Baker
homepage: https://github.com/PostgREST/postgrest
maintainer: cred+github@begriffs.com
category: Web
homepage: https://postgrest.org
maintainer: Steve Chávez <stevechavezast@gmail.com>
bug-reports: https://github.com/PostgREST/postgrest/issues
category: Executable, PostgreSQL, Network APIs
extra-source-files: CHANGELOG.md
build-type: Simple
cabal-version: >=1.10
source-repository head
@@ -30,11 +32,12 @@ executable postgrest
"-with-rtsopts=-N -I2"
default-language: Haskell2010
build-depends: auto-update
, base
, hasql
, hasql-pool
, base >= 4.8 && < 4.10
, hasql >= 1.3 && < 1.4
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7 && < 0.8
, postgrest
, protolude
, protolude == 0.2.2
, text
, time
, warp
@@ -51,7 +54,7 @@ library
default-extensions: OverloadedStrings, QuasiQuotes, NoImplicitPrelude
build-depends: aeson
, ansi-wl-pprint
, base >= 4.8 && < 6
, base >= 4.8 && < 4.10
, base64-bytestring
, bytestring
, case-insensitive
@@ -62,24 +65,23 @@ library
, contravariant-extras
, either
, gitrev
, hasql
, hasql-pool
, hasql-transaction
, hasql >= 1.3 && < 1.4
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7 && < 0.8
, heredoc
, HTTP
, http-types
, insert-ordered-containers
, interpolatedstring-perl6
, jose
, jose == 0.7.0.0
, lens
, lens-aeson
, network-uri
, optparse-applicative >= 0.13 && < 0.15
, parsec
, protolude >= 0.2
, protolude == 0.2.2
, Ranged-sets == 0.3.0
, regex-tdfa
, safe
, scientific
, swagger2
, text
@@ -122,8 +124,11 @@ Test-Suite spec
, Feature.ConcurrentSpec
, Feature.CorsSpec
, Feature.DeleteSpec
, Feature.ExtraSearchPathSpec
, Feature.InsertSpec
, Feature.JsonOperatorSpec
, Feature.NoJwtSpec
, Feature.PgVersion95Spec
, Feature.PgVersion96Spec
, Feature.ProxySpec
, Feature.QueryLimitedSpec
@@ -142,15 +147,16 @@ Test-Suite spec
, aeson-qq
, async
, auto-update
, base
, base >= 4.8 && < 4.10
, bytestring
, base64-bytestring
, case-insensitive
, cassava
, containers
, contravariant
, hasql
, hasql-pool
, hasql >= 1.3 && < 1.4
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7 && < 0.8
, heredoc
, hjsonschema == 1.5.0.1
, hspec
@@ -162,7 +168,7 @@ Test-Suite spec
, monad-control
, postgrest
, process
, protolude
, protolude == 0.2.2
, regex-tdfa
, time
, transformers-base
+5 -6
View File
@@ -105,13 +105,12 @@ userApiRequest :: Schema -> Request -> RequestBody -> Either ApiRequestError Api
userApiRequest schema req reqBody
| isTargetingProc && method `notElem` ["GET", "POST"] = Left ActionInappropriate
| topLevelRange == emptyRange = Left InvalidRange
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) undefined payload
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
| otherwise = Right ApiRequest {
iAction = action
, iTarget = target
, iRange = ranges
, iAccepts = fromMaybe [CTAny] $
map decodeContentType . parseHttpAccept <$> lookupHeader "accept"
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
, iPayload = relevantPayload
, iPreferRepresentation = representation
, iPreferSingleObjectParameter = singleObject
@@ -130,7 +129,7 @@ userApiRequest schema req reqBody
$ rawQueryString req
, iJWT = tokenStr
, iHeaders = [ (toS $ CI.foldedCase k, toS v) | (k,v) <- hdrs, k /= hAuthorization, k /= hCookie]
, iCookies = fromMaybe [] $ parseCookiesText <$> lookupHeader "Cookie"
, iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie"
}
where
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
@@ -143,7 +142,7 @@ userApiRequest schema req reqBody
((<> ".") <$> "not":M.keys operators) ++
((<> "(") <$> M.keys ftsOperators)
isEmbedPath = T.isInfixOf "."
isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path
isTargetingProc = (== Just "rpc") $ listToMaybe path
payload =
case (decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type", action) of
(_, ActionInvoke{isReadOnly=True}) ->
@@ -215,7 +214,7 @@ userApiRequest schema req reqBody
limitParams :: M.HashMap ByteString NonnegRange
limitParams = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe =<< (toS <$> v)) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]
offsetParams :: M.HashMap ByteString NonnegRange
offsetParams = M.fromList [(toS (replaceLast "limit" k), fromMaybe allRange (rangeGeq <$> (readMaybe =<< (toS <$> v)))) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
offsetParams = M.fromList [(toS (replaceLast "limit" k), maybe allRange rangeGeq (readMaybe =<< (toS <$> v))) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
urlRange = M.unionWith f limitParams offsetParams
where
+14 -15
View File
@@ -35,7 +35,7 @@ import PostgREST.ApiRequest ( ApiRequest(..), ContentType(..)
, mutuallyAgreeable
, userApiRequest
)
import PostgREST.Auth (jwtClaims, containsRole, parseJWK)
import PostgREST.Auth (jwtClaims, containsRole, parseSecret)
import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure
import PostgREST.DbRequestBuilder( readRequest
@@ -61,12 +61,11 @@ import PostgREST.OpenAPI
import Data.Function (id)
import Protolude hiding (intercalate, Proxy)
import Safe (headMay)
postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application
postgrest conf refDbStructure pool getTime worker =
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle
jwtSecret = parseJWK <$> configJwtSecret conf in
jwtSecret = parseSecret <$> configJwtSecret conf in
middle $ \ req respond -> do
time <- getTime
@@ -103,14 +102,14 @@ findProc qi payloadKeys paramsAsSingleObject allProcs =
else payloadKeys `S.isSubsetOf` S.fromList (pgaName <$> pdArgs x))
) <$> procs
transactionMode :: Maybe ProcDescription -> Action -> H.Mode
transactionMode :: Maybe ProcDescription -> Action -> HT.Mode
transactionMode proc action =
case action of
ActionRead -> HT.Read
ActionInfo -> HT.Read
ActionInspect -> HT.Read
ActionInvoke{isReadOnly=False} ->
let v = fromMaybe Volatile $ pdVolatility <$> proc in
let v = maybe Volatile pdVolatility proc in
if v == Stable || v == Immutable
then HT.Read
else HT.Write
@@ -132,7 +131,7 @@ app dbStructure proc conf apiRequest =
Right ((q, cq), bField) -> do
let stm = createReadStatement q cq (contentType == CTSingularJSON) shouldCount
(contentType == CTTextCSV) bField
row <- H.query () stm
row <- H.statement () stm
let (tableTotal, queryTotal, _ , body) = row
(status, contentRange) = rangeHeader queryTotal tableTotal
canonical = iCanonicalQS apiRequest
@@ -163,7 +162,7 @@ app dbStructure proc conf apiRequest =
stm = createWriteStatement sq mq
(contentType == CTSingularJSON) isSingle
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols
row <- H.query (toS pjRaw) stm
row <- H.statement (toS pjRaw) stm
let (_, _, fs, body) = extractQueryResult row
headers = catMaybes [
if null fs
@@ -192,7 +191,7 @@ app dbStructure proc conf apiRequest =
let stm = createWriteStatement sq mq
(contentType == CTSingularJSON) False (contentType == CTTextCSV)
(iPreferRepresentation apiRequest) []
row <- H.query (toS pjRaw) stm
row <- H.statement (toS pjRaw) stm
let (_, queryTotal, _, body) = extractQueryResult row
if contentType == CTSingularJSON
&& queryTotal /= 1
@@ -225,7 +224,7 @@ app dbStructure proc conf apiRequest =
else if S.fromList colNames /= pjKeys
then return $ simpleError status400 [] "You must specify all columns in the payload when using PUT"
else do
row <- H.query (toS pjRaw) $
row <- H.statement (toS pjRaw) $
createWriteStatement sq mq (contentType == CTSingularJSON) False
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) []
let (_, queryTotal, _, body) = extractQueryResult row
@@ -249,7 +248,7 @@ app dbStructure proc conf apiRequest =
(contentType == CTSingularJSON) False
(contentType == CTTextCSV)
(iPreferRepresentation apiRequest) []
row <- H.query mempty stm
row <- H.statement mempty stm
let (_, queryTotal, _, body) = extractQueryResult row
r = contentRangeH 1 0 $
toInteger <$> if shouldCount then Just queryTotal else Nothing
@@ -287,8 +286,8 @@ app dbStructure proc conf apiRequest =
PJObject -> True
PJArray _ -> False
singular = contentType == CTSingularJSON
specifiedPgArgs = filter ((`S.member` pjKeys) . pgaName) $ fromMaybe [] (pdArgs <$> proc)
row <- H.query (toS pjRaw) $
specifiedPgArgs = filter ((`S.member` pjKeys) . pgaName) $ maybe [] pdArgs proc
row <- H.statement (toS pjRaw) $
callProc qi specifiedPgArgs returnsScalar q cq shouldCount
singular (iPreferSingleObjectParameter apiRequest)
(contentType == CTTextCSV)
@@ -317,7 +316,7 @@ app dbStructure proc conf apiRequest =
toTableInfo :: [Table] -> [(Table, [Column], [Text])]
toTableInfo = map (\t -> let (s, tn) = (tableSchema t, tableName t) in (t, tableCols dbStructure s tn, tablePKCols dbStructure s tn))
encodeApi ti sd procs = encodeOpenAPI (concat $ M.elems procs) (toTableInfo ti) uri' sd $ dbPrimaryKeys dbStructure
body <- encodeApi <$> H.query schema accessibleTables <*> H.query schema schemaDescription <*> H.query schema accessibleProcs
body <- encodeApi <$> H.statement schema accessibleTables <*> H.statement schema schemaDescription <*> H.statement schema accessibleProcs
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body
_ -> return notFound
@@ -398,8 +397,8 @@ contentRangeH lower upper total =
rangeString
| totalNotZero && fromInRange = show lower <> "-" <> show upper
| otherwise = "*"
totalString = fromMaybe "*" (show <$> total)
totalNotZero = fromMaybe True ((/=) 0 <$> total)
totalString = maybe "*" show total
totalNotZero = maybe True (0 /=) total
fromInRange = lower <= upper
extractQueryResult :: Maybe ResultsWithCount -> ResultsWithCount
+24 -13
View File
@@ -16,10 +16,11 @@ module PostgREST.Auth (
containsRole
, jwtClaims
, JWTAttempt(..)
, parseJWK
, parseSecret
) where
import Control.Lens.Operators
import Control.Lens (set)
import qualified Data.Aeson as JSON
import qualified Data.HashMap.Strict as M
import Data.Time.Clock (UTCTime)
@@ -42,13 +43,13 @@ data JWTAttempt = JWTInvalid JWTError
Receives the JWT secret and audience (from config) and a JWT and returns a map
of JWT claims.
-}
jwtClaims :: Maybe JWK -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt
jwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt
jwtClaims _ _ "" _ _ = return $ JWTClaims M.empty
jwtClaims secret audience payload time jspath =
case secret of
Nothing -> return JWTMissingSecret
Just s -> do
let validation = defaultJWTValidationSettings (maybe (const True) (==) audience)
let validation = set allowedSkew 1 $ defaultJWTValidationSettings (maybe (const True) (==) audience)
eJwt <- runExceptT $ do
jwt <- decodeCompact payload
verifyClaimsAt validation s time jwt
@@ -82,19 +83,29 @@ containsRole :: JWTAttempt -> Bool
containsRole (JWTClaims claims) = M.member "role" claims
containsRole _ = False
parseJWK :: ByteString -> JWK
parseJWK str =
fromMaybe (hs256jwk str) (JSON.decode (toS str) :: Maybe JWK)
{-|
Parse `jwt-secret` configuration option and turn into a JWKSet.
There are three ways to specify `jwt-secret`: text secret, JSON Web Key
(JWK), or JSON Web Key Set (JWKS). The first two are converted into a JWKSet
with one key and the last is converted as is.
-}
parseSecret :: ByteString -> JWKSet
parseSecret str =
fromMaybe (maybe secret (\jwk' -> JWKSet [jwk']) maybeJWK)
maybeJWKSet
where
maybeJWKSet = JSON.decode (toS str) :: Maybe JWKSet
maybeJWK = JSON.decode (toS str) :: Maybe JWK
secret = JWKSet [jwkFromSecret str]
{-|
Internal helper to generate HMAC-SHA256. When the jwt key in the
config file is a simple string rather than a JWK object, we'll
apply this function to it.
Internal helper to generate a symmetric HMAC-SHA256 JWK from a text secret.
-}
hs256jwk :: ByteString -> JWK
hs256jwk key =
jwkFromSecret :: ByteString -> JWK
jwkFromSecret key =
fromKeyMaterial km
& jwkUse .~ Just Sig
& jwkAlg .~ (Just $ JWSAlg HS256)
& jwkUse ?~ Sig
& jwkAlg ?~ JWSAlg HS256
where
km = OctKeyMaterial (OctKeyParameters (JOSE.Types.Base64Octets key))
+21 -28
View File
@@ -18,9 +18,6 @@ module PostgREST.Config ( prettyVersion
, docsVersion
, readOptions
, corsPolicy
, minimumPgVersion
, pgVersion95
, pgVersion96
, AppConfig (..)
)
where
@@ -42,7 +39,7 @@ import Data.Scientific (floatingOrInteger)
import Data.String (String)
import Data.Text (dropAround,
intercalate, lines,
strip, take)
strip, take, splitOn)
import Data.Text.Encoding (encodeUtf8)
import Data.Text.IO (hPutStrLn)
import Data.Version (versionBranch)
@@ -52,7 +49,7 @@ import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
import Options.Applicative hiding (str)
import Paths_postgrest (version)
import PostgREST.Parsers (pRoleClaimKey)
import PostgREST.Types (PgVersion(..), ApiRequestError(..),
import PostgREST.Types (ApiRequestError(..),
JSPath, JSPathExp(..))
import Protolude hiding (hPutStrLn, take,
intercalate, (<>))
@@ -81,11 +78,12 @@ data AppConfig = AppConfig {
, configQuiet :: Bool
, configSettings :: [(Text, Text)]
, configRoleClaimKey :: Either ApiRequestError JSPath
, configExtraSearchPath :: [Text]
}
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
["GET", "POST", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"] ["Authorization"] Nothing
(Just $ 60*60*24) False False True
-- | CORS policy to be used in by Wai Cors middleware
@@ -132,7 +130,7 @@ readOptions = do
<*> C.key "db-anon-role"
<*> (mfilter (/= "") <$> C.key "server-proxy-uri")
<*> C.key "db-schema"
<*> (fromMaybe "*4" . mfilter (/= "") <$> C.key "server-host")
<*> (fromMaybe "127.0.0.1" . mfilter (/= "") <$> C.key "server-host")
<*> (fromMaybe 3000 . join . fmap coerceInt <$> C.key "server-port")
<*> (fmap encodeUtf8 . mfilter (/= "") <$> C.key "jwt-secret")
<*> (fromMaybe False . join . fmap coerceBool <$> C.key "secret-is-base64")
@@ -141,8 +139,9 @@ readOptions = do
<*> (join . fmap coerceInt <$> C.key "max-rows")
<*> (mfilter (/= "") <$> C.key "pre-request")
<*> pure False
<*> (fmap parsedPairToTextPair <$> C.subassocs "app.settings")
<*> (fmap (fmap coerceText) <$> C.subassocs "app.settings")
<*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> C.key "role-claim-key")
<*> (maybe ["public"] splitExtraSearchPath <$> C.key "db-extra-search-path")
case mAppConf of
Nothing -> do
@@ -152,13 +151,6 @@ readOptions = do
return appConf
where
parsedPairToTextPair :: (Name, Value) -> (Text, Text)
parsedPairToTextPair (k, v) = (k, newValue)
where
newValue = case v of
String textVal -> textVal
_ -> show v
parseJwtAudience :: Name -> C.ConfigParserM (Maybe StringOrURI)
parseJwtAudience k =
C.key k >>= \case
@@ -168,6 +160,10 @@ readOptions = do
(Just "") -> pure Nothing
aud' -> pure aud'
coerceText :: Value -> Text
coerceText (String s) = s
coerceText v = show v
coerceInt :: (Read i, Integral i) => Value -> Maybe i
coerceInt (Number x) = rightToMaybe $ floatingOrInteger x
coerceInt (String x) = readMaybe $ toS x
@@ -182,6 +178,10 @@ readOptions = do
parseRoleClaimKey (String s) = pRoleClaimKey s
parseRoleClaimKey v = pRoleClaimKey $ show v
splitExtraSearchPath :: Value -> [Text]
splitExtraSearchPath (String s) = strip <$> splitOn "," s
splitExtraSearchPath _ = []
opts = info (helper <*> pathParser) $
fullDesc
<> progDesc (
@@ -205,17 +205,17 @@ readOptions = do
exampleCfg :: Doc
exampleCfg = vsep . map (text . toS) . lines $
[str|db-uri = "postgres://user:pass@localhost:5432/dbname"
|db-schema = "public"
|db-schema = "public" # this schema gets added to the search_path of every request
|db-anon-role = "postgres"
|db-pool = 10
|
|server-host = "*4"
|server-host = "127.0.0.1"
|server-port = 3000
|
|## base url for swagger output
|# server-proxy-uri = ""
|
|## choose a secret to enable JWT auth
|## choose a secret, JSON Web Key (or set) to enable JWT auth
|## (use "@filename" to load from separate file)
|# jwt-secret = "foo"
|# secret-is-base64 = false
@@ -229,6 +229,9 @@ readOptions = do
|
|## jspath to the role claim key
|# role-claim-key = ".role"
|
|## extra schemas to add to the search_path of every request
|# db-extra-search-path = "extensions, util"
|]
pathParser :: Parser FilePath
@@ -236,13 +239,3 @@ pathParser =
strArgument $
metavar "FILENAME" <>
help "Path to configuration file"
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
minimumPgVersion :: PgVersion
minimumPgVersion = PgVersion 90400 "9.4"
pgVersion96 :: PgVersion
pgVersion96 = PgVersion 90600 "9.6"
pgVersion95 :: PgVersion
pgVersion95 = PgVersion 90500 "9.5"
+7 -7
View File
@@ -36,7 +36,7 @@ import PostgREST.Parsers
import PostgREST.RangeQuery (NonnegRange, restrictRange, allRange)
import PostgREST.Types
import Protolude hiding (from, dropWhile, drop)
import Protolude hiding (from)
import Text.Regex.TDFA ((=~))
import Unsafe (unsafeHead)
@@ -90,7 +90,7 @@ readRequest maxRows allRels proc apiRequest =
-- 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 a the "returning *" part which is wrapped with a "with"
-- 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 _ _)
@@ -225,13 +225,13 @@ addJoinConditions schema (Node node@(query, nodeProps@(_, relation, _, _, _)) fo
addJoinCond jc rq@Select{joinConditions=jcs} = rq{joinConditions=jc:jcs}
getJoinConditions :: Relation -> [JoinCondition]
getJoinConditions (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fcs typ lt lc1 lc2) =
getJoinConditions (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) =
if | typ == Child || typ == Parent ->
zipWith (toJoinCondition tN ftN) cols fcs
zipWith (toJoinCondition tN ftN) cols fCols
| typ == Many ->
let ltN = fromMaybe "" (tableName <$> lt) in
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fcs (fromMaybe [] lc2)
| typ == Root -> undefined
let ltN = maybe "" tableName lt in
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2)
| typ == Root -> witness
where
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
toJoinCondition tb ftb c fc =
+151 -172
View File
@@ -10,12 +10,11 @@ module PostgREST.DbStructure (
, accessibleProcs
, schemaDescription
, getPgVersion
, fillSessionWithSettings
) where
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Query as H
import qualified Hasql.Statement as H
import Control.Applicative
import qualified Data.HashMap.Strict as M
@@ -26,26 +25,25 @@ import Data.Text (split, strip,
splitOn)
import qualified Data.Text as T
import qualified Hasql.Session as H
import qualified Hasql.Transaction as HT
import PostgREST.Types
import Text.InterpolatedString.Perl6 (q)
import Text.InterpolatedString.Perl6 (q, qc)
import GHC.Exts (groupWith)
import Protolude
import Unsafe (unsafeHead)
import Data.Functor.Contravariant (contramap)
import Contravariant.Extras (contrazip2)
getDbStructure :: Schema -> PgVersion -> H.Session DbStructure
getDbStructure :: Schema -> PgVersion -> HT.Transaction DbStructure
getDbStructure schema pgVer = do
tabs <- H.query () allTables
cols <- H.query schema $ allColumns tabs
syns <- H.query () $ allSynonyms cols
childRels <- H.query () $ allChildRelations tabs cols
keys <- H.query () $ allPrimaryKeys tabs
procs <- H.query schema allProcs
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 $ addViewRelations syns childRels
let rels = addManyToManyRelations . addParentRelations $ addViewChildRelations syns childRels
cols' = addForeignKeys rels cols
keys' = addViewPrimaryKeys syns keys
@@ -60,70 +58,70 @@ getDbStructure schema pgVer = do
decodeTables :: HD.Result [Table]
decodeTables =
HD.rowsList tblRow
HD.rowList tblRow
where
tblRow = Table <$> HD.value HD.text
<*> HD.value HD.text
<*> HD.nullableValue HD.text
<*> HD.value HD.bool
tblRow = Table <$> HD.column HD.text
<*> HD.column HD.text
<*> HD.nullableColumn HD.text
<*> HD.column HD.bool
decodeColumns :: [Table] -> HD.Result [Column]
decodeColumns tables =
mapMaybe (columnFromRow tables) <$> HD.rowsList colRow
mapMaybe (columnFromRow tables) <$> HD.rowList colRow
where
colRow =
(,,,,,,,,,,,)
<$> HD.value HD.text <*> HD.value HD.text
<*> HD.value HD.text <*> HD.nullableValue HD.text
<*> HD.value HD.int4 <*> HD.value HD.bool
<*> HD.value HD.text <*> HD.value HD.bool
<*> HD.nullableValue HD.int4
<*> HD.nullableValue HD.int4
<*> HD.nullableValue HD.text
<*> HD.nullableValue HD.text
<$> HD.column HD.text <*> HD.column HD.text
<*> HD.column HD.text <*> HD.nullableColumn HD.text
<*> HD.column HD.int4 <*> HD.column HD.bool
<*> HD.column HD.text <*> HD.column HD.bool
<*> HD.nullableColumn HD.int4
<*> HD.nullableColumn HD.int4
<*> HD.nullableColumn HD.text
<*> HD.nullableColumn HD.text
decodeRelations :: [Table] -> [Column] -> HD.Result [Relation]
decodeRelations tables cols =
mapMaybe (relationFromRow tables cols) <$> HD.rowsList relRow
mapMaybe (relationFromRow tables cols) <$> HD.rowList relRow
where
relRow = (,,,,,)
<$> HD.value HD.text
<*> HD.value HD.text
<*> HD.value (HD.array (HD.arrayDimension replicateM (HD.arrayValue HD.text)))
<*> HD.value HD.text
<*> HD.value HD.text
<*> HD.value (HD.array (HD.arrayDimension replicateM (HD.arrayValue HD.text)))
<$> HD.column HD.text
<*> HD.column HD.text
<*> HD.column (HD.array (HD.dimension replicateM (HD.element HD.text)))
<*> HD.column HD.text
<*> HD.column HD.text
<*> HD.column (HD.array (HD.dimension replicateM (HD.element HD.text)))
decodePks :: [Table] -> HD.Result [PrimaryKey]
decodePks tables =
mapMaybe (pkFromRow tables) <$> HD.rowsList pkRow
mapMaybe (pkFromRow tables) <$> HD.rowList pkRow
where
pkRow = (,,) <$> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text
pkRow = (,,) <$> HD.column HD.text <*> HD.column HD.text <*> HD.column HD.text
decodeSynonyms :: [Column] -> HD.Result [Synonym]
decodeSynonyms cols =
mapMaybe (synonymFromRow cols) <$> HD.rowsList synRow
mapMaybe (synonymFromRow cols) <$> HD.rowList synRow
where
synRow = (,,,,,)
<$> HD.value HD.text <*> HD.value HD.text
<*> HD.value HD.text <*> HD.value HD.text
<*> HD.value HD.text <*> HD.value HD.text
<$> HD.column HD.text <*> HD.column HD.text
<*> HD.column HD.text <*> HD.column HD.text
<*> HD.column HD.text <*> HD.column HD.text
decodeProcs :: HD.Result (M.HashMap Text [ProcDescription])
decodeProcs =
-- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addName) <$> HD.rowsList tblRow
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addName) <$> HD.rowList tblRow
where
tblRow = ProcDescription
<$> HD.value HD.text
<*> HD.nullableValue HD.text
<*> (parseArgs <$> HD.value HD.text)
<$> HD.column HD.text
<*> HD.nullableColumn HD.text
<*> (parseArgs <$> HD.column HD.text)
<*> (parseRetType
<$> HD.value HD.text
<*> HD.value HD.text
<*> HD.value HD.bool
<*> HD.value HD.char)
<*> (parseVolatility <$> HD.value HD.char)
<$> HD.column HD.text
<*> HD.column HD.text
<*> HD.column HD.bool
<*> HD.column HD.char)
<*> (parseVolatility <$> HD.column HD.char)
addName :: ProcDescription -> (Text, ProcDescription)
addName pd = (pdName pd, pd)
@@ -159,11 +157,11 @@ decodeProcs =
| v == 's' = Stable
| otherwise = Volatile -- only 'v' can happen here
allProcs :: H.Query Schema (M.HashMap Text [ProcDescription])
allProcs = H.statement (toS procsSqlQuery) (HE.value HE.text) decodeProcs True
allProcs :: H.Statement Schema (M.HashMap Text [ProcDescription])
allProcs = H.Statement (toS procsSqlQuery) (HE.param HE.text) decodeProcs True
accessibleProcs :: H.Query Schema (M.HashMap Text [ProcDescription])
accessibleProcs = H.statement (toS sql) (HE.value HE.text) decodeProcs True
accessibleProcs :: H.Statement Schema (M.HashMap Text [ProcDescription])
accessibleProcs = H.Statement (toS sql) (HE.param HE.text) decodeProcs True
where
sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
@@ -186,9 +184,9 @@ procsSqlQuery = [q|
WHERE pn.nspname = $1
|]
schemaDescription :: H.Query Schema (Maybe Text)
schemaDescription :: H.Statement Schema (Maybe Text)
schemaDescription =
H.statement sql (HE.value HE.text) (join <$> HD.maybeRow (HD.nullableValue HD.text)) True
H.Statement sql (HE.param HE.text) (join <$> HD.rowMaybe (HD.nullableColumn HD.text)) True
where
sql = [q|
select
@@ -199,9 +197,9 @@ schemaDescription =
where
n.nspname = $1 |]
accessibleTables :: H.Query Schema [Table]
accessibleTables :: H.Statement Schema [Table]
accessibleTables =
H.statement sql (HE.value HE.text) decodeTables True
H.Statement sql (HE.param HE.text) decodeTables True
where
sql = [q|
select
@@ -247,32 +245,32 @@ Having a Relation{relTable=t1, relColumns=[c1], relFTable=t2, relFColumns=[c2],
t1.c1------t2.c2
When only having a t1_view.c1 synonym, we need to add a View to Table Relation
When only having a t1_view.c1 synonym, we need to add a View to Table Child Relation
t1.c1----t2.c2 t1.c1----------t2.c2
-> --------/
-> ________/
/
t1_view.c1 t1_view.c1
When only having a t2_view.c2 synonym, we need to add a Table to View Relation
When only having a t2_view.c2 synonym, we need to add a Table to View Child Relation
t1.c1----t2.c2 t1.c1----------t2.c2
-> \--------
-> \________
\
t2_view.c2 t2_view.c1
When having t1_view.c1 and a t2_view.c2 synonyms, we need to add a View to View Relation in addition to the prior
When having t1_view.c1 and a t2_view.c2 synonyms, we need to add a View to View Child Relation in addition to the prior
t1.c1----t2.c2 t1.c1----------t2.c2
-> \--------/
-> \________/
/ \
t1_view.c1 t2_view.c2 t1_view.c1-------t2_view.c1
The logic for composite pks is similar just need to make sure all the Relation columns have synonyms.
-}
addViewRelations :: [Synonym] -> [Relation] -> [Relation]
addViewRelations allSyns = concatMap (\rel ->
addViewChildRelations :: [Synonym] -> [Relation] -> [Relation]
addViewChildRelations allSyns = concatMap (\rel ->
rel : case rel of
Relation{relType=Child, relTable, relColumns, relFTable, relFColumns} ->
@@ -283,18 +281,22 @@ addViewRelations allSyns = concatMap (\rel ->
fColsSyns = colSynsGroupedByView relFColumns
getView :: [Synonym] -> Table
getView = colTable . snd . unsafeHead
syns `allSynsOf` cols = S.fromList (fst <$> syns) == S.fromList cols in
syns `allSynsOf` cols = S.fromList (fst <$> syns) == S.fromList cols
-- Relation is dependent on the order of relColumns and relFColumns to get the join conditions right in the generated query.
-- So we need to change the order of the synonyms to match the relColumns
-- This could be avoided if the Relation type is improved with a structure that maintains the association of relColumns and relFColumns
syns `sortAccordingTo` columns = sortOn (\(k, _) -> L.lookup k $ zip columns [0::Int ..]) syns in
-- View Table Relations
[Relation (getView syns) (snd <$> syns) relFTable relFColumns Child Nothing Nothing Nothing
-- View Table Child Relations
[Relation (getView syns) (snd <$> syns `sortAccordingTo` relColumns) relFTable relFColumns Child Nothing Nothing Nothing
| syns <- colsSyns, syns `allSynsOf` relColumns] ++
-- Table View Relations
[Relation relTable relColumns (getView fSyns) (snd <$> fSyns) Child Nothing Nothing Nothing
-- Table View Child Relations
[Relation relTable relColumns (getView fSyns) (snd <$> fSyns `sortAccordingTo` relFColumns) Child Nothing Nothing Nothing
| fSyns <- fColsSyns, fSyns `allSynsOf` relFColumns] ++
-- View View Relations
[Relation (getView syns) (snd <$> syns) (getView fSyns) (snd <$> fSyns) Child Nothing Nothing Nothing
-- View View Child Relations
[Relation (getView syns) (snd <$> syns `sortAccordingTo` relColumns) (getView fSyns) (snd <$> fSyns `sortAccordingTo` relFColumns) Child Nothing Nothing Nothing
| syns <- colsSyns, fSyns <- fColsSyns, syns `allSynsOf` relColumns, fSyns `allSynsOf` relFColumns]
_ -> [])
@@ -328,9 +330,9 @@ addViewPrimaryKeys syns = concatMap (\pk ->
filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) syns in
pk : viewPks)
allTables :: H.Query () [Table]
allTables :: H.Statement () [Table]
allTables =
H.statement sql HE.unit decodeTables True
H.Statement sql HE.unit decodeTables True
where
sql = [q|
SELECT
@@ -351,9 +353,9 @@ allTables =
GROUP BY table_schema, table_name, insertable
ORDER BY table_schema, table_name |]
allColumns :: [Table] -> H.Query Schema [Column]
allColumns :: [Table] -> H.Statement Schema [Column]
allColumns tabs =
H.statement sql (HE.value HE.text) (decodeColumns tabs) True
H.Statement sql (HE.param HE.text) (decodeColumns tabs) True
where
sql = [q|
SELECT DISTINCT
@@ -388,7 +390,7 @@ allColumns tabs =
pg_catalog.pg_namespace n
WHERE
r.contype IN ('f', 'p')
AND c.relkind IN ('r', 'v', 'f', 'mv')
AND c.relkind IN ('r', 'v', 'f', 'm')
AND r.conrelid = c.oid
AND c.relnamespace = n.oid
AND n.nspname NOT IN ('pg_catalog', 'information_schema', $1)
@@ -492,7 +494,7 @@ allColumns tabs =
NOT pg_is_other_temp_schema(nc.oid)
AND a.attnum > 0
AND NOT a.attisdropped
AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char", 'm'::"char"]))
AND (nc.nspname = $1 OR kc.r_oid IS NOT NULL) /*--filter only columns that are FK/PK or in the api schema */
/*--AND (pg_has_role(c.relowner, 'USAGE'::text) OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/
)
@@ -536,11 +538,11 @@ columnFromRow tabs (s, t, n, desc, pos, nul, typ, u, l, p, d, e) = buildColumn <
buildColumn tbl = Column tbl n desc pos nul typ u l p d (parseEnum e) Nothing
table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
parseEnum :: Maybe Text -> [Text]
parseEnum str = fromMaybe [] $ split (==',') <$> str
parseEnum = maybe [] (split (==','))
allChildRelations :: [Table] -> [Column] -> H.Query () [Relation]
allChildRelations :: [Table] -> [Column] -> H.Statement () [Relation]
allChildRelations tabs cols =
H.statement sql HE.unit (decodeRelations tabs cols) True
H.Statement sql HE.unit (decodeRelations tabs cols) True
where
sql = [q|
SELECT ns1.nspname AS table_schema,
@@ -579,9 +581,9 @@ relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
cols = mapM (findCol rs rt) rcs
colsF = mapM (findCol frs frt) frcs
allPrimaryKeys :: [Table] -> H.Query () [PrimaryKey]
allPrimaryKeys :: [Table] -> H.Statement () [PrimaryKey]
allPrimaryKeys tabs =
H.statement sql HE.unit (decodePks tabs) True
H.Statement sql HE.unit (decodePks tabs) True
where
sql = [q|
/*
@@ -689,80 +691,71 @@ pkFromRow :: [Table] -> (Schema, Text, Text) -> Maybe PrimaryKey
pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n
where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
allSynonyms :: [Column] -> H.Query () [Synonym]
allSynonyms cols =
H.statement sql HE.unit (decodeSynonyms cols) True
where
-- query explanation at https://gist.github.com/ruslantalpa/2eab8c930a65e8043d8f
sql = [q|
with view_columns as (
allSynonyms :: [Column] -> PgVersion -> H.Statement Schema [Synonym]
allSynonyms cols pgVer =
H.Statement sql (HE.param HE.text) (decodeSynonyms cols) True
-- query explanation at https://gist.github.com/steve-chavez/7ee0e6590cddafb532e5f00c46275569
where
subselectRegex :: Text
subselectRegex | pgVer < pgVersion100 = ":subselect {.*?:constraintDeps <>} :location"
| otherwise = ":subselect {.*?:stmt_len 0} :location"
sql = [qc|
with
views as (
select
c.oid as view_oid,
a.attname::information_schema.sql_identifier as column_name
from pg_attribute a
join pg_class c on a.attrelid = c.oid
join pg_namespace nc on c.relnamespace = nc.oid
where
not pg_is_other_temp_schema(nc.oid)
and a.attnum > 0
and not a.attisdropped
and (c.relkind = 'v'::"char")
and nc.nspname not in ('information_schema', 'pg_catalog')
),
view_column_usage as (
select distinct
v.oid as view_oid,
nv.nspname::information_schema.sql_identifier as view_schema,
v.relname::information_schema.sql_identifier as view_name,
nt.nspname::information_schema.sql_identifier as table_schema,
t.relname::information_schema.sql_identifier as table_name,
a.attname::information_schema.sql_identifier as column_name,
pg_get_viewdef(v.oid)::information_schema.character_data as view_definition
from pg_namespace nv
join pg_class v on nv.oid = v.relnamespace
join pg_depend dv on v.oid = dv.refobjid
join pg_depend dt on dv.objid = dt.objid
join pg_class t on dt.refobjid = t.oid
join pg_namespace nt on t.relnamespace = nt.oid
join pg_attribute a on t.oid = a.attrelid and dt.refobjsubid = a.attnum
where
nv.nspname not in ('information_schema', 'pg_catalog')
and v.relkind = 'v'::"char"
and dv.refclassid = 'pg_class'::regclass::oid
and dv.classid = 'pg_rewrite'::regclass::oid
and dv.deptype = 'i'::"char"
and dv.refobjid <> dt.refobjid
and dt.classid = 'pg_rewrite'::regclass::oid
and dt.refclassid = 'pg_class'::regclass::oid
and (t.relkind = any (array['r'::"char", 'v'::"char", 'f'::"char"]))
),
candidates as (
n.nspname as view_schema,
c.relname as view_name,
r.ev_action as view_definition
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
join pg_rewrite r on r.ev_class = c.oid
where (c.relkind = 'v'::char) and n.nspname = $1
),
removed_subselects as(
select
vcu.*,
(
select case when match is not null then coalesce(match[8], match[7], match[4]) end
from regexp_matches(
CONCAT('SELECT ', SPLIT_PART(vcu.view_definition, 'SELECT', 2)),
CONCAT('SELECT.*?((',vcu.table_name,')|(\w+))\.(', vcu.column_name, ')(\s+AS\s+("([^"]+)"|([^, \n\t]+)))?.*?FROM.*?(',vcu.table_schema,'\.|)(\2|',vcu.table_name,'\s+(as\s)?\3)'),
'nsi'
) match
) as view_column_name
from view_column_usage as vcu
)
select
c.table_schema,
c.table_name,
c.column_name as table_column_name,
c.view_schema,
c.view_name,
c.view_column_name
from view_columns as vc, candidates as c
where
vc.view_oid = c.view_oid
and vc.column_name = c.view_column_name
order by c.view_schema, c.view_name, c.table_name, c.view_column_name
|]
view_schema, view_name,
regexp_replace(view_definition, '{subselectRegex}', '', 'g') as x
from views
),
target_lists as(
select
view_schema, view_name,
regexp_split_to_array(x, 'targetList') as x
from removed_subselects
),
last_target_list_wo_tail as(
select
view_schema, view_name,
(regexp_split_to_array(x[array_upper(x, 1)], ':onConflict'))[1] as x
from target_lists
),
target_entries as(
select
view_schema, view_name,
unnest(regexp_split_to_array(x, 'TARGETENTRY')) as entry
from last_target_list_wo_tail
),
results as(
select
view_schema, view_name,
substring(entry from ':resname (.*?) :') as view_colum_name,
substring(entry from ':resorigtbl (.*?) :') as resorigtbl,
substring(entry from ':resorigcol (.*?) :') as resorigcol
from target_entries
)
select
sch.nspname as table_schema,
tbl.relname as table_name,
col.attname as table_column_name,
res.view_schema,
res.view_name,
res.view_colum_name
from results res
join pg_class tbl on tbl.oid::text = res.resorigtbl
join pg_attribute col on col.attrelid = tbl.oid and col.attnum::text = res.resorigcol
join pg_namespace sch on sch.oid = tbl.relnamespace
where resorigtbl <> '0'
order by view_schema, view_name, view_colum_name; |]
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe Synonym
synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
@@ -772,21 +765,7 @@ synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols
getPgVersion :: H.Session PgVersion
getPgVersion = H.query () $ H.statement sql HE.unit versionRow False
getPgVersion = H.statement () $ H.Statement sql HE.unit versionRow False
where
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
versionRow = HD.singleRow $ PgVersion <$> HD.value HD.int4 <*> HD.value HD.text
fillSessionWithSettings :: [(Text, Text)] -> H.Session ()
fillSessionWithSettings settings =
-- Send all of the config settings to the set_config function, using pgsql's `unnest` to transform arrays of values
H.query settings $ H.statement "SELECT set_config(k, v, false) FROM unnest($1, $2) AS f1(k, v)" encoder HD.unit False
where
-- Take a list of (key, value) pairs and encode each as an array to later bind to the query
-- see Insert Many section at https://hackage.haskell.org/package/hasql-1.1.1/docs/Hasql-Encoders.html
encoder = contramap L.unzip $ contrazip2 (vector HE.text) (vector HE.text)
where
vector value =
HE.value $ HE.array $ HE.arrayDimension foldl' $ HE.arrayValue value
versionRow = HD.singleRow $ PgVersion <$> HD.column HD.int4 <*> HD.column HD.text
+7 -4
View File
@@ -118,7 +118,10 @@ instance JSON.ToJSON P.UsageError where
"details" .= (toS $ fromMaybe "" e :: Text)]
toJSON (P.SessionError e) = JSON.toJSON e -- H.Error
instance JSON.ToJSON H.Error where
instance JSON.ToJSON H.QueryError where
toJSON (H.QueryError _ _ e) = JSON.toJSON e
instance JSON.ToJSON H.CommandError where
toJSON (H.ResultError (H.ServerError c m d h)) = case toS c of
'P':'T':_ ->
JSON.object [
@@ -154,7 +157,7 @@ instance JSON.ToJSON H.Error where
httpStatus :: Bool -> P.UsageError -> HT.Status
httpStatus _ (P.ConnectionError _) = HT.status503
httpStatus authed (P.SessionError (H.ResultError (H.ServerError c m _ _))) =
httpStatus authed (P.SessionError (H.QueryError _ _ (H.ResultError (H.ServerError c m _ _)))) =
case toS c of
'0':'8':_ -> HT.status503 -- pg connection err
'0':'9':_ -> HT.status500 -- triggered action exception
@@ -184,5 +187,5 @@ httpStatus authed (P.SessionError (H.ResultError (H.ServerError c m _ _))) =
"42501" -> if authed then HT.status403 else HT.status401 -- insufficient privilege
'P':'T':n -> fromMaybe HT.status500 (HT.mkStatus <$> readMaybe n <*> pure m)
_ -> HT.status400
httpStatus _ (P.SessionError (H.ResultError _)) = HT.status500
httpStatus _ (P.SessionError (H.ClientError _)) = HT.status503
httpStatus _ (P.SessionError (H.QueryError _ _ (H.ResultError _))) = HT.status500
httpStatus _ (P.SessionError (H.QueryError _ _ (H.ClientError _))) = HT.status503
+10 -9
View File
@@ -19,9 +19,9 @@ import PostgREST.ApiRequest (ApiRequest(..))
import PostgREST.Auth (JWTAttempt(..))
import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (simpleError)
import PostgREST.QueryBuilder (pgFmtLit, unquoted, pgFmtEnvVar)
import PostgREST.QueryBuilder (unquoted, pgFmtSetLocal, pgFmtSetLocalSearchPath)
import Protolude hiding (concat, null)
import Protolude
runWithClaims :: AppConfig -> JWTAttempt ->
(ApiRequest -> H.Transaction Response) ->
@@ -32,16 +32,17 @@ runWithClaims conf eClaims app req =
JWTInvalid e -> return $ unauthed $ show e
JWTMissingSecret -> return $ simpleError status500 [] "Server lacks JWT secret"
JWTClaims claims -> do
H.sql $ toS.mconcat $ setSchemaSql ++ setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql
H.sql $ toS . mconcat $ setSearchPathSql : setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql ++ appSettingsSql
mapM_ H.sql customReqCheck
app req
where
headersSql = map (pgFmtEnvVar "request.header.") $ iHeaders req
cookiesSql = map (pgFmtEnvVar "request.cookie.") $ iCookies req
claimsSql = map (pgFmtEnvVar "request.jwt.claim.") [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
setRoleSql = maybeToList $
(\r -> "set local role " <> r <> ";") . toS . pgFmtLit . unquoted <$> M.lookup "role" claimsWithRole
setSchemaSql = ["set schema " <> pgFmtLit (configSchema conf) <> ";"] :: [Text]
headersSql = pgFmtSetLocal "request.header." <$> iHeaders req
cookiesSql = pgFmtSetLocal "request.cookie." <$> iCookies req
claimsSql = pgFmtSetLocal "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
appSettingsSql = pgFmtSetLocal mempty <$> configSettings conf
setRoleSql = maybeToList $ (\x ->
pgFmtSetLocal mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole
setSearchPathSql = pgFmtSetLocalSearchPath $ configSchema conf : configExtraSearchPath conf
-- role claim defaults to anon if not specified in jwt
claimsWithRole = M.union claims (M.singleton "role" anon)
anon = JSON.String . toS $ configAnonRole conf
+13 -4
View File
@@ -42,7 +42,8 @@ makeTableDef pks (t, cs, _) =
(tn, (mempty :: Schema)
& description .~ tableDescription t
& type_ .~ SwaggerObject
& properties .~ fromList (map (makeProperty pks) cs))
& properties .~ fromList (map (makeProperty pks) cs)
& required .~ map colName (filter (not . colNullable) cs))
makeProperty :: [PrimaryKey] -> Column -> (Text, Referenced Schema)
makeProperty pks c = (colName c, Inline s)
@@ -59,7 +60,7 @@ makeProperty pks c = (colName c, Inline s)
]
d =
if length n > 1 then
Just $ append (fromMaybe "" ((`append` "\n\n") <$> colDescription c)) (intercalate "\n" n)
Just $ append (maybe "" (`append` "\n\n") $ colDescription c) (intercalate "\n" n)
else
colDescription c
s =
@@ -197,8 +198,11 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
& at 206 ?~ "Partial Content"
& at 200 ?~ Inline ((mempty :: Response)
& description .~ "OK"
& schema ?~ (Ref $ Reference $ tableName t)
& schema ?~ Inline (mempty
& type_ .~ SwaggerArray
& items ?~ (SwaggerItemsObject $ Ref $ Reference $ tableName t)
)
)
postOp = tOp
& parameters .~ map ref ["body." <> tn, "preferReturn"]
& at 201 ?~ "Created"
@@ -219,8 +223,13 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
makeProcPathItem :: ProcDescription -> (FilePath, PathItem)
makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
where
-- Use first line of proc description as summary; rest as description (if present)
-- We strip leading newlines from description so that users can include a blank line between summary and description
(pSum, pDesc) = fmap fst &&& fmap (dropWhile (=='\n') . snd) $
breakOn "\n" <$> pdDescription pd
postOp = (mempty :: Operation)
& description .~ pdDescription pd
& summary .~ pSum
& description .~ mfilter (/="") pDesc
& parameters .~ makeProcParam pd
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
& produces ?~ makeMimeList [CTApplicationJSON, CTSingularJSON]
+33 -21
View File
@@ -3,6 +3,7 @@ module PostgREST.Parsers where
import Protolude hiding (try, intercalate, replace, option)
import Control.Monad ((>>))
import Data.Foldable (foldl1)
import Data.Functor (($>))
import qualified Data.HashMap.Strict as M
import Data.Text (intercalate, replace, strip)
import Data.List (init, last)
@@ -57,7 +58,7 @@ lexeme p = ws *> p <* ws
pTreePath :: Parser (EmbedPath, Field)
pTreePath = do
p <- pFieldName `sepBy1` pDelimiter
jp <- optionMaybe pJsonPath
jp <- option [] pJsonPath
return (init p, (last p, jp))
pFieldForest :: Parser [Tree SelectItem]
@@ -68,26 +69,39 @@ pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
Node <$> pFieldSelect <*> pure []
pStar :: Parser Text
pStar = toS <$> (string "*" *> pure ("*"::ByteString))
pStar = toS <$> (string "*" $> ("*"::ByteString))
pFieldName :: Parser Text
pFieldName = do
matches <- (many1 (letter <|> digit <|> oneOf "_") `sepBy1` dash) <?> "field name (* or [a..z0..9_])"
return $ intercalate "-" $ map toS matches
pFieldName =
pQuotedValue <|>
intercalate "-" . map toS <$> (many1 (letter <|> digit <|> oneOf "_ ") `sepBy1` dash) <?>
"field name (* or [a..z0..9_])"
where
isDash :: GenParser Char st ()
isDash = try ( char '-' >> notFollowedBy (char '>') )
dash :: Parser Char
dash = isDash *> pure '-'
dash = isDash $> '-'
pJsonPathStep :: Parser Text
pJsonPathStep = toS <$> try (string "->" *> pFieldName)
pJsonPath :: Parser JsonPath
pJsonPath = many pJsonOperation
where
pJsonOperation :: Parser JsonOperation
pJsonOperation = pJsonArrow <*> pJsonOperand
pJsonPath :: Parser [Text]
pJsonPath = (<>) <$> many pJsonPathStep <*> ( (:[]) <$> (string "->>" *> pFieldName) )
pJsonArrow =
try (string "->>" $> J2Arrow) <|>
try (string "->" $> JArrow)
pJsonOperand =
let pJKey = JKey . toS <$> pFieldName
pJIdx = JIdx . toS <$> ((:) <$> option '+' (char '-') <*> many1 digit) <* pEnd
pEnd = try (void $ lookAhead (string "->")) <|>
try (void $ lookAhead (string "::")) <|>
try eof in
try pJIdx <|> try pJKey
pField :: Parser Field
pField = lexeme $ (,) <$> pFieldName <*> optionMaybe pJsonPath
pField = lexeme $ (,) <$> pFieldName <*> option [] pJsonPath
aliasSeparator :: Parser ()
aliasSeparator = char ':' >> notFollowedBy (char ':')
@@ -112,7 +126,7 @@ pFieldSelect = lexeme $
)
<|> do
s <- pStar
return ((s, Nothing), Nothing, Nothing, Nothing)
return ((s, []), Nothing, Nothing, Nothing)
pOpExpr :: Parser SingleVal -> Parser OpExpr
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
@@ -139,10 +153,10 @@ pListVal :: Parser ListVal
pListVal = lexeme (char '(') *> pListElement `sepBy1` char ',' <* lexeme (char ')')
pListElement :: Parser Text
pListElement = try pQuotedValue <|> (toS <$> many (noneOf ",)"))
pListElement = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> (toS <$> many (noneOf ",)"))
pQuotedValue :: Parser Text
pQuotedValue = toS <$> (char '"' *> many (noneOf "\"") <* char '"' <* notFollowedBy (noneOf ",)"))
pQuotedValue = toS <$> (char '"' *> many (noneOf "\"") <* char '"')
pDelimiter :: Parser Char
pDelimiter = char '.' <?> "delimiter (.)"
@@ -172,16 +186,16 @@ pLogicTree = Stmnt <$> try pLogicFilter
pLogicFilter :: Parser Filter
pLogicFilter = Filter <$> pField <* pDelimiter <*> pOpExpr pLogicSingleVal
pNot :: Parser Bool
pNot = try (string "not" *> pDelimiter *> pure True)
pNot = try (string "not" *> pDelimiter $> True)
<|> pure False
<?> "negation operator (not)"
pLogicOp :: Parser LogicOperator
pLogicOp = try (string "and" *> pure And)
<|> string "or" *> pure Or
pLogicOp = try (string "and" $> And)
<|> string "or" $> Or
<?> "logic operator (and, or)"
pLogicSingleVal :: Parser SingleVal
pLogicSingleVal = try pQuotedValue <|> try pPgArray <|> (toS <$> many (noneOf ",)"))
pLogicSingleVal = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> try pPgArray <|> (toS <$> many (noneOf ",)"))
where
pPgArray :: Parser Text
pPgArray = do
@@ -222,9 +236,7 @@ pJSPath = toJSPath <$> (period *> pPath `sepBy` period <* eof)
pPath = (,) <$> pJSPKey <*> optionMaybe pJSPIdx
pJSPKey :: Parser Text
pJSPKey = toS <$> (many1 (alphaNum <|> oneOf "_$@") <|> pQuoted) <?> "attribute name [a..z0..9_$@])"
where
pQuoted = char '"' *> many (noneOf "\"") <* char '"'
pJSPKey = toS <$> many1 (alphaNum <|> oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
pJSPIdx :: Parser Int
pJSPIdx = char '[' *> (read <$> many1 digit) <* char ']' <?> "array index [0..n]"
+57 -43
View File
@@ -1,7 +1,8 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-|
Module : PostgREST.QueryBuilder
Description : PostgREST SQL generating functions.
@@ -22,16 +23,16 @@ module PostgREST.QueryBuilder (
, requestToCountQuery
, unquoted
, ResultsWithCount
, pgFmtEnvVar
, pgFmtSetLocal
, pgFmtSetLocalSearchPath
) where
import qualified Hasql.Query as H
import qualified Hasql.Statement as H
import qualified Hasql.Encoders as HE
import qualified Hasql.Decoders as HD
import qualified Data.Aeson as JSON
import PostgREST.Config (pgVersion96)
import PostgREST.RangeQuery (rangeLimit, rangeOffset, allRange)
import qualified Data.HashMap.Strict as HM
import Data.Maybe
@@ -47,7 +48,7 @@ import Data.Scientific ( FPFormat (..)
, formatScientific
, isInteger
)
import Protolude hiding (from, intercalate, ord, cast, replace)
import Protolude hiding ( intercalate, cast, replace)
import PostgREST.ApiRequest (PreferRepresentation (..))
{-| The generic query result format used by API responses. The location header
@@ -57,10 +58,10 @@ import PostgREST.ApiRequest (PreferRepresentation (..))
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString)
standardRow :: HD.Row ResultsWithCount
standardRow = (,,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
<*> HD.value header <*> HD.value HD.bytea
standardRow = (,,,) <$> HD.nullableColumn HD.int8 <*> HD.column HD.int8
<*> HD.column header <*> HD.column HD.bytea
where
header = HD.array $ HD.arrayDimension replicateM $ HD.arrayValue HD.bytea
header = HD.array $ HD.dimension replicateM $ HD.element HD.bytea
noLocationF :: Text
noLocationF = "array[]::text[]"
@@ -75,10 +76,10 @@ decodeStandard =
decodeStandardMay :: HD.Result (Maybe ResultsWithCount)
decodeStandardMay =
HD.maybeRow standardRow
HD.rowMaybe standardRow
createReadStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool -> Maybe FieldName ->
H.Query () ResultsWithCount
H.Statement () ResultsWithCount
createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField =
unicodeStatement sql HE.unit decodeStandard False
where
@@ -101,9 +102,9 @@ createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField
createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool ->
PreferRepresentation -> [Text] ->
H.Query ByteString (Maybe ResultsWithCount)
H.Statement ByteString (Maybe ResultsWithCount)
createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys =
unicodeStatement sql (HE.value HE.unknown) decodeStandardMay True
unicodeStatement sql (HE.param HE.unknown) decodeStandardMay True
where
sql = case rep of
@@ -138,9 +139,9 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys
type ProcResults = (Maybe Int64, Int64, ByteString, ByteString)
callProc :: QualifiedIdentifier -> [PgArg] -> Bool -> SqlQuery -> SqlQuery -> Bool ->
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool -> PgVersion ->
H.Query ByteString (Maybe ProcResults)
H.Statement ByteString (Maybe ProcResults)
callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle paramsAsSingleObject asCsv asBinary binaryField isObject pgVer =
unicodeStatement sql (HE.value HE.unknown) decodeProc True
unicodeStatement sql (HE.param HE.unknown) decodeProc True
where
sql =
if returnsScalar then [qc|
@@ -172,18 +173,18 @@ callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle para
unwords [
"_args_record AS (",
"SELECT * FROM " <> (if isObject then "json_to_record" else "json_to_recordset") <> "($1)",
"AS _(" <> intercalate ", " ((\a -> pgaName a <> " " <> pgaType a) <$> pgArgs) <> ")",
"AS _(" <> intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " " <> pgaType a) <$> pgArgs) <> ")",
")"]
, intercalate ", " ((\a -> pgaName a <> " := (SELECT " <> pgaName a <> " FROM _args_record)") <$> pgArgs))
, intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM _args_record)") <$> pgArgs))
countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text
_procName = qiName qi
responseHeaders =
if pgVer >= pgVersion96
then "coalesce(nullif(current_setting('response.headers', true), ''), '[]')" :: Text -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
else "'[]'" :: Text
decodeProc = HD.maybeRow procRow
procRow = (,,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
<*> HD.value HD.bytea <*> HD.value HD.bytea
decodeProc = HD.rowMaybe procRow
procRow = (,,,) <$> HD.nullableColumn HD.int8 <*> HD.column HD.int8
<*> HD.column HD.bytea <*> HD.column HD.bytea
scalarBodyF
| asBinary = asBinaryF _procName
| otherwise = "(row_to_json(_postgrest_t)->" <> pgFmtLit _procName <> ")::character varying"
@@ -207,7 +208,7 @@ pgFmtLit x =
else slashed
requestToCountQuery :: Schema -> DbRequest -> SqlQuery
requestToCountQuery _ (DbMutate _) = undefined
requestToCountQuery _ (DbMutate _) = witness
requestToCountQuery schema (DbRead (Node (Select{where_=logicForest}, (mainTbl, _, _, _, _)) _)) =
unwords [
"SELECT pg_catalog.count(*)",
@@ -229,7 +230,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest
("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (isParent || range == allRange) ]
where
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
mainTbl = maybe nodeName (tableName . relTable) maybeRelation
isSelfJoin = maybe False (\r -> relType r /= Root && relTable r == relFTable r) maybeRelation
(qi, tables, joinConds) =
let depthAlias name dpth = if dpth /= 0 then name <> "_" <> show dpth else name in -- Root node doesn't get aliased
@@ -272,7 +273,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest
--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 _ _ = undefined
getQueryParts _ _ = witness
requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType pKeys) onConflct logicForest returnings)) =
unwords [
("WITH " <> ignoredBody) `emptyOnFalse` not payloadIsEmpty,
@@ -288,7 +289,7 @@ requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType
-- Only used for PUT
("WHERE " <> intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier "" "_") <$> logicForest)) `emptyOnFalse` null logicForest],
maybe "" (\x -> (
"ON CONFLICT(" <> intercalate ", " pkCols <> ") " <> case x of
"ON CONFLICT(" <> intercalate ", " (pgFmtIdent <$> pkCols) <> ") " <> case x of
IgnoreDuplicates ->
"DO NOTHING"
MergeDuplicates ->
@@ -380,8 +381,8 @@ fromQi t = (if s == "" then "" else pgFmtIdent s <> ".") <> pgFmtIdent n
n = qiName t
s = qiSchema t
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Query a b
unicodeStatement = H.statement . T.encodeUtf8
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Statement a b
unicodeStatement = H.Statement . T.encodeUtf8
emptyOnFalse :: Text -> Bool -> Text
emptyOnFalse val cond = if cond then "" else val
@@ -394,8 +395,8 @@ pgFmtField :: QualifiedIdentifier -> Field -> SqlFragment
pgFmtField table (c, jp) = pgFmtColumn table c <> pgFmtJsonPath jp
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SqlFragment
pgFmtSelectItem table (f@(_, jp), Nothing, alias, _) = pgFmtField table f <> pgFmtAs jp alias
pgFmtSelectItem table (f@(_, jp), Just cast, alias, _) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAs jp alias
pgFmtSelectItem table (f@(fName, jp), Nothing, alias, _) = pgFmtField table f <> pgFmtAs fName jp alias
pgFmtSelectItem table (f@(fName, jp), Just cast, alias, _) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAs fName jp alias
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> SqlFragment
pgFmtOrderTerm qi ot = unwords [
@@ -413,7 +414,7 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
In vals -> pgFmtField table fld <> " " <>
let emptyValForIn = "= any('{}') " in -- Workaround because for postgresql "col IN ()" is invalid syntax, we instead do "col = any('{}')"
case ((&&) (length vals == 1) . T.null) <$> headMay vals of
case (&&) (length vals == 1) . T.null <$> headMay vals of
Just False -> sqlOperator "in" <> "(" <> intercalate ", " (map unknownLiteral vals) <> ") "
Just True -> emptyValForIn
Nothing -> emptyValForIn
@@ -447,21 +448,34 @@ pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <>
where notOp = if hasNot then "NOT" else ""
pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs )
pgFmtJsonPath _ = ""
pgFmtJsonPath :: JsonPath -> SqlFragment
pgFmtJsonPath = \case
[] -> ""
(JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
(J2Arrow x:xs) -> "->>" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
where
pgFmtJsonOperand (JKey k) = pgFmtLit k
pgFmtJsonOperand (JIdx i) = pgFmtLit i <> "::int"
pgFmtAs :: Maybe JsonPath -> Maybe Alias -> SqlFragment
pgFmtAs Nothing Nothing = ""
pgFmtAs (Just xx) Nothing = case lastMay xx of
Just alias -> " AS " <> pgFmtIdent alias
pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment
pgFmtAs _ [] Nothing = ""
pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of
Just (JKey key) -> " AS " <> pgFmtIdent key
Just (JIdx _) -> " AS " <> pgFmtIdent (fromMaybe fName lastKey)
-- We get the lastKey because on:
-- `select=data->1->mycol->>2`, we need to show the result as [ {"mycol": ..}, {"mycol": ..} ]
-- `select=data->3`, we need to show the result as [ {"data": ..}, {"data": ..} ]
where lastKey = jVal <$> find (\case JKey{} -> True; _ -> False) (jOp <$> reverse jp)
Nothing -> ""
pgFmtAs _ (Just alias) = " AS " <> pgFmtIdent alias
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
pgFmtEnvVar :: Text -> (Text, Text) -> SqlFragment
pgFmtEnvVar prefix (k, v) =
"set local " <> pgFmtIdent (prefix <> k) <> " = " <> pgFmtLit v <> ";"
pgFmtSetLocal :: Text -> (Text, Text) -> SqlFragment
pgFmtSetLocal prefix (k, v) =
"SET LOCAL " <> pgFmtIdent (prefix <> k) <> " = " <> pgFmtLit v <> ";"
pgFmtSetLocalSearchPath :: [Text] -> SqlFragment
pgFmtSetLocalSearchPath vals =
"SET LOCAL search_path = " <> intercalate ", " (pgFmtLit <$> vals) <> ";"
trimNullChars :: Text -> Text
trimNullChars = T.takeWhile (/= '\x0')
+3 -4
View File
@@ -32,14 +32,13 @@ rangeParse range = do
case listToMaybe (range =~ rangeRegex :: [[BS.ByteString]]) of
Just parsedRange ->
let [_, mLower, mUpper] = readMaybe . toS <$> parsedRange
lower = fromMaybe emptyRange (rangeGeq <$> mLower)
upper = fromMaybe allRange (rangeLeq <$> mUpper) in
lower = maybe emptyRange rangeGeq mLower
upper = maybe allRange rangeLeq mUpper in
rangeIntersection lower upper
Nothing -> allRange
rangeRequested :: RequestHeaders -> NonnegRange
rangeRequested headers = fromMaybe allRange $
rangeParse <$> lookup hRange headers
rangeRequested headers = maybe allRange rangeParse $ lookup hRange headers
restrictRange :: Maybe Integer -> NonnegRange -> NonnegRange
restrictRange Nothing r = r
+30 -3
View File
@@ -144,6 +144,9 @@ data RelationType = Child | Parent | Many | Root deriving (Show, Eq)
The name 'Relation' here is used with the meaning
"What is the relation between the current node and the parent node".
It has nothing to do with PostgreSQL referring to tables/views as relations.
The order of the relColumns and relFColumns should be maintained to get
the join conditions right.
TODO merge relColumns and relFColumns to a tuple or Data.Bimap
-}
data Relation = Relation {
relTable :: Table
@@ -238,8 +241,16 @@ instance Show LogicOperator where
data LogicTree = Expr Bool LogicOperator [LogicTree] | Stmnt Filter deriving (Show, Eq)
type FieldName = Text
type JsonPath = [Text]
type Field = (FieldName, Maybe JsonPath)
{-|
Json path operations as specified in https://www.postgresql.org/docs/9.4/static/functions-json.html
-}
type JsonPath = [JsonOperation]
-- | Represents the single arrow `->` or double arrow `->>` operators
data JsonOperation = JArrow{jOp :: JsonOperand} | J2Arrow{jOp :: JsonOperand} deriving (Show, Eq)
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text because we reuse our escaping functons and let pg do the casting with '1'::int
data JsonOperand = JKey{jVal :: Text} | JIdx{jVal :: Text} deriving (Show, Eq)
type Field = (FieldName, JsonPath)
type Alias = Text
type Cast = Text
type NodeName = Text
@@ -309,7 +320,23 @@ toMime (CTOther ct) = ct
data PgVersion = PgVersion {
pgvNum :: Int32
, pgvName :: Text
} deriving (Eq, Ord, Show)
} deriving (Eq, Show)
instance Ord PgVersion where
(PgVersion v1 _) `compare` (PgVersion v2 _) = v1 `compare` v2
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
minimumPgVersion :: PgVersion
minimumPgVersion = PgVersion 90400 "9.4"
pgVersion95 :: PgVersion
pgVersion95 = PgVersion 90500 "9.5"
pgVersion96 :: PgVersion
pgVersion96 = PgVersion 90600 "9.6"
pgVersion100 :: PgVersion
pgVersion100 = PgVersion 100000 "10"
sourceCTEName :: SqlFragment
sourceCTEName = "pg_source"
+6 -4
View File
@@ -5,11 +5,13 @@ extra-deps:
- hjsonpointer-1.1.1
- hjsonschema-1.5.0.1
- Ranged-sets-0.3.0
- protolude-0.2
- hasql-1.1
- hasql-pool-0.4.3
- hasql-transaction-0.5.2
- protolude-0.2.2
- hasql-1.3
- hasql-pool-0.5
- hasql-transaction-0.7
- text-builder-0.5.1.1
- jose-0.7.0.0
- postgresql-libpq-0.9.4.1
ghc-options:
postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints
nix:
+1 -5
View File
@@ -167,7 +167,7 @@ spec =
context "used with POST" $
it "includes related data with filters" $
request methodPost "/child_entities?entities.or=(id.eq.2,id.eq.3)&select=id,entities(id)"
request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id"
[("Prefer", "return=representation")]
[json|[{"id":4,"name":"entity 4","parent_id":1},
{"id":5,"name":"entity 5","parent_id":2},
@@ -193,10 +193,6 @@ spec =
it "can query columns that begin with and/or reserved words" $
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
it "can query jsonb columns" $
get "/grandchild_entities?or=(jsonb_col->a->>b.eq.foo, jsonb_col->>b.eq.bar)&select=id" `shouldRespondWith`
[json|[{id: 4}, {id: 5}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] }
it "fails when using IN without () and provides meaningful error message" $
get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
[json|{
+1 -1
View File
@@ -8,7 +8,7 @@ import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
import Protolude
-- }}}
spec :: SpecWith Application
+1 -1
View File
@@ -8,7 +8,7 @@ import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
import Protolude
-- }}}
spec :: SpecWith Application
+1 -1
View File
@@ -8,7 +8,7 @@ import Control.Monad.Base
import Control.Monad.Trans.Control
import Control.Concurrent.Async (mapConcurrently)
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai.Internal
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
+2 -2
View File
@@ -11,7 +11,7 @@ import SpecHelper
import Network.HTTP.Types
import Network.Wai (Application)
import Protolude hiding (get)
import Protolude
-- }}}
spec :: SpecWith Application
@@ -45,7 +45,7 @@ spec =
"true"
respHeaders `shouldSatisfy` matchHeader
"Access-Control-Allow-Methods"
"GET, POST, PATCH, DELETE, OPTIONS, HEAD"
"GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD"
respHeaders `shouldSatisfy` matchHeader
"Access-Control-Allow-Headers"
"Authentication, Foo, Bar, Accept, Accept-Language, Content-Language"
+37
View File
@@ -0,0 +1,37 @@
module Feature.ExtraSearchPathSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude
spec :: SpecWith Application
spec = describe "extra search path" $ do
it "finds the ltree <@ operator on the public schema" $
request methodGet "/ltree_sample?path=cd.Top.Science.Astronomy" [] ""
`shouldRespondWith` [json|[
{"path":"Top.Science.Astronomy"},
{"path":"Top.Science.Astronomy.Astrophysics"},
{"path":"Top.Science.Astronomy.Cosmology"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "finds the ltree nlevel function on the public schema, used through a computed column" $
request methodGet "/ltree_sample?select=number_of_labels&path=eq.Top.Science" [] ""
`shouldRespondWith` [json|[{"number_of_labels":2}]|]
{ matchHeaders = [matchContentTypeJson] }
it "finds the isn = operator on the extensions schema" $
request methodGet "/isn_sample?id=eq.978-0-393-04002-9&select=name" [] ""
`shouldRespondWith` [json|[{"name":"Mathematics: From the Birth of Numbers"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "finds the isn is_valid function on the extensions schema" $
request methodGet "/rpc/is_valid_isbn?input=978-0-393-04002-9" [] ""
`shouldRespondWith` [json|true|]
{ matchHeaders = [matchContentTypeJson] }
-10
View File
@@ -390,16 +390,6 @@ spec = do
[json| [{ a: "keepme", b: null }] |]
{ matchHeaders = [matchContentTypeJson] }
it "can set a json column to escaped value" $ do
_ <- post "/json" [json| { data: {"escaped":"bar"} } |]
request methodPatch "/json?data->>escaped=eq.bar"
[("Prefer", "return=representation")]
[json| { "data": { "escaped":" \"bar" } } |]
`shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |]
{ matchStatus = 200
, matchHeaders = []
}
it "can update based on a computed column" $
request methodPatch
"/items?always_true=eq.false"
+189
View File
@@ -0,0 +1,189 @@
module Feature.JsonOperatorSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
spec :: SpecWith Application
spec = describe "json and jsonb operators" $ do
context "Shaping response with select parameter" $ do
it "obtains a json subfield one level with casting" $
get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith`
[json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
{ matchHeaders = [matchContentTypeJson] }
it "renames json subfield one level with casting" $
get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith`
[json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
{ matchHeaders = [matchContentTypeJson] }
it "fails on bad casting (data of the wrong format)" $
get "/complex_items?select=settings->foo->>bar::integer"
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
{ matchStatus = 400 , matchHeaders = [] }
it "obtains a json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
[json| [{"bar":"baz"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "renames json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith`
[json| [{"myBar":"baz"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "obtains a json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith`
[json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now
{ matchHeaders = [matchContentTypeJson] }
it "renames json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith`
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
{ matchHeaders = [matchContentTypeJson] }
-- TODO the status code for the error is 404, this is because 42883 represents undefined function
-- this works fine for /rpc/unexistent requests, but for this case a 500 seems more appropriate
it "fails when a double arrow ->> is followed with a single arrow ->" $ do
get "/json_arr?select=data->>c->1"
`shouldRespondWith` [json|
{"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.",
"details":null,"code":"42883","message":"operator does not exist: text -> integer"} |]
{ matchStatus = 404 , matchHeaders = [] }
get "/json_arr?select=data->>c->b"
`shouldRespondWith` [json|
{"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.",
"details":null,"code":"42883","message":"operator does not exist: text -> unknown"} |]
{ matchStatus = 404 , matchHeaders = [] }
context "with array index" $ do
it "can get array of ints and alias/cast it" $ do
get "/json_arr?select=data->>0::int&id=in.(1,2)" `shouldRespondWith`
[json| [{"data":1}, {"data":4}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=idx0:data->>0::int,idx1:data->>1::int&id=in.(1,2)" `shouldRespondWith`
[json| [{"idx0":1,"idx1":2}, {"idx0":4,"idx1":5}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can get nested array of ints" $ do
get "/json_arr?select=data->0->>1::int&id=in.(3,4)" `shouldRespondWith`
[json| [{"data":8}, {"data":7}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->0->0->>1::int&id=in.(3,4)" `shouldRespondWith`
[json| [{"data":null}, {"data":6}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can get array of objects" $ do
get "/json_arr?select=data->0->>a&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":"A"}, {"a":"[1,2,3]"}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->0->a->>2&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":null}, {"a":"3"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can get array in object keys" $ do
get "/json_arr?select=data->c->>0::json&id=in.(7,8)" `shouldRespondWith`
[json| [{"c":1}, {"c":{"d": [4,5,6,7,8]}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->c->0->d->>4::int&id=in.(7,8)" `shouldRespondWith`
[json| [{"d":null}, {"d":8}] |]
{ matchHeaders = [matchContentTypeJson] }
it "only treats well formed numbers as indexes" $
get "/json_arr?select=data->0->0xy1->1->23-xy-45->1->xy-6->>0::int&id=eq.9" `shouldRespondWith`
[json| [{"xy-6":3}] |]
{ matchHeaders = [matchContentTypeJson] }
context "finishing json path with single arrow ->" $ do
it "works when finishing with a key" $ do
get "/json_arr?select=data->c&id=in.(7,8)" `shouldRespondWith`
[json| [{"c":[1,2,3]}, {"c":[{"d": [4,5,6,7,8]}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":"A"}, {"a":[1,2,3]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "works when finishing with an index" $ do
get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":"A"}, {"a":[1,2,3]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->c->0->d&id=eq.8" `shouldRespondWith`
[json| [{"d":[4,5,6,7,8]}] |]
{ matchHeaders = [matchContentTypeJson] }
context "filtering response" $ do
it "can filter by properties inside json column" $ do
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
[json| [] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter by properties inside json column using not" $
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
[json| [] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter by properties inside json column using ->>" $
get "/json?data->>id=eq.1" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can be filtered with and/or" $
get "/grandchild_entities?or=(jsonb_col->a->>b.eq.foo, jsonb_col->>b.eq.bar)&select=id" `shouldRespondWith`
[json|[{id: 4}, {id: 5}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] }
it "can filter by array indexes" $ do
get "/json_arr?select=data&data->>0=eq.1" `shouldRespondWith`
[json| [{"data":[1, 2, 3]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->1->>2=eq.13" `shouldRespondWith`
[json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->1->>b=eq.B" `shouldRespondWith`
[json| [{"data":[{"a": "A"}, {"b": "B"}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->1->b->>1=eq.5" `shouldRespondWith`
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter jsonb" $ do
get "/jsonb_test?data=eq.{\"e\":1}" `shouldRespondWith`
[json| [{"id":4,"data":{"e": 1}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/jsonb_test?data->a=eq.{\"b\":2}" `shouldRespondWith`
[json| [{"id":1,"data":{"a": {"b": 2}}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/jsonb_test?data->c=eq.[1,2,3]" `shouldRespondWith`
[json| [{"id":2,"data":{"c": [1, 2, 3]}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/jsonb_test?data->0=eq.{\"d\":\"test\"}" `shouldRespondWith`
[json| [{"id":3,"data":[{"d": "test"}]}] |]
{ matchHeaders = [matchContentTypeJson] }
context "ordering response" $ do
it "orders by a json column property asc" $
get "/json?order=data->>id.asc" `shouldRespondWith`
[json| [{"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}, {"data": {"id": 3}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "orders by a json column with two level property nulls first" $
get "/json?order=data->foo->>bar.nullsfirst" `shouldRespondWith`
[json| [{"data": {"id": 3}}, {"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
context "Patching record, in a nonempty table" $
it "can set a json column to escaped value" $ do
_ <- post "/json" [json| { data: {"escaped":"bar"} } |]
request methodPatch "/json?data->>escaped=eq.bar"
[("Prefer", "return=representation")]
[json| { "data": { "escaped":" \"bar" } } |]
`shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |]
{ matchStatus = 200 , matchHeaders = [] }
+1 -1
View File
@@ -8,7 +8,7 @@ import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
import Protolude
-- }}}
spec :: SpecWith Application
+55
View File
@@ -0,0 +1,55 @@
module Feature.PgVersion95Spec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
spec :: SpecWith Application
spec = describe "features supported on PostgreSQL 9.5" $
context "json array negative index" $ do
it "can select with negative indexes" $ do
get "/json_arr?select=data->>-1::int&id=in.(1,2)" `shouldRespondWith`
[json| [{"data":3}, {"data":6}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->0->>-2::int&id=in.(3,4)" `shouldRespondWith`
[json| [{"data":8}, {"data":7}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->-2->>a&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":"A"}, {"a":"[1,2,3]"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter with negative indexes" $ do
get "/json_arr?select=data&data->>-3=eq.1" `shouldRespondWith`
[json| [{"data":[1, 2, 3]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->-1->>-3=eq.11" `shouldRespondWith`
[json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->-1->>b=eq.B" `shouldRespondWith`
[json| [{"data":[{"a": "A"}, {"b": "B"}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->-1->b->>-1=eq.5" `shouldRespondWith`
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "should fail on badly formed negatives" $ do
get "/json_arr?select=data->>-78xy" `shouldRespondWith`
[json|
{"details": "unexpected 'x' expecting digit, \"->\", \"::\" or end of input",
"message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)"} |]
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->>--34" `shouldRespondWith`
[json|
{"details": "unexpected \"-\" expecting digit",
"message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)"} |]
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->>-xy-4" `shouldRespondWith`
[json|
{"details":"unexpected \"x\" expecting digit",
"message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)"} |]
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
+1 -1
View File
@@ -1,6 +1,6 @@
module Feature.PgVersion96Spec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
+2 -2
View File
@@ -1,12 +1,12 @@
module Feature.ProxySpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
import Protolude
spec :: SpecWith Application
spec =
+1 -1
View File
@@ -1,6 +1,6 @@
module Feature.QueryLimitedSpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
+131 -108
View File
@@ -1,6 +1,6 @@
module Feature.QuerySpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
@@ -216,23 +216,6 @@ spec = do
[json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "json subfield one level with casting (json)" $
get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith`
[json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
{ matchHeaders = [matchContentTypeJson] }
it "rename json subfield one level with casting (json)" $
get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith`
[json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
{ matchHeaders = [matchContentTypeJson] }
it "fails on bad casting (data of the wrong format)" $
get "/complex_items?select=settings->foo->>bar::integer"
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
{ matchStatus = 400
, matchHeaders = []
}
it "fails on bad casting (wrong cast type)" $
get "/complex_items?select=id::fakecolumntype"
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
@@ -240,34 +223,12 @@ spec = do
, matchHeaders = []
}
it "json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
[json| [{"bar":"baz"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "rename json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith`
[json| [{"myBar":"baz"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith`
[json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now
{ matchHeaders = [matchContentTypeJson] }
it "rename json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith`
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
{ matchHeaders = [matchContentTypeJson] }
it "requesting parents and children" $
get "/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting parent without specifying primary key" $ do
it "requesting parent without specifying primary key" $
get "/projects?select=name,client(name)" `shouldRespondWith`
[json|[
{"name":"Windows 7","client":{"name": "Microsoft"}},
@@ -277,9 +238,6 @@ spec = do
{"name":"Orphan","client":null}
]|]
{ matchHeaders = [matchContentTypeJson] }
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 "requesting parent and renaming primary key" $
get "/projects?select=name,client(clientId:id,name)" `shouldRespondWith`
@@ -351,40 +309,11 @@ spec = do
[json|[{"id":1,"tasks":[{"id":1},{"id":2},{"id":3},{"id":4}]},{"id":2,"tasks":[{"id":5},{"id":6},{"id":7}]},{"id":3,"tasks":[{"id":1},{"id":5}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting parents and children on views" $
get "/projects_view?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting parents and children on views with renamed keys" $
get "/projects_view_alt?t_id=eq.1&select=t_id, name, clients(*), tasks(id, name)" `shouldRespondWith`
[json|[{"t_id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "detects parent relations when having many views of a private table" $ do
get "/books?select=title,author(name)&id=eq.5" `shouldRespondWith`
[json|[ { "title": "Farenheit 451", "author": { "name": "Ray Bradbury" } } ]|]
{ matchHeaders = [matchContentTypeJson] }
get "/forties_books?select=title,author(name)&limit=1" `shouldRespondWith`
[json|[ { "title": "1984", "author": { "name": "George Orwell" } } ]|]
{ matchHeaders = [matchContentTypeJson] }
get "/fifties_books?select=title,author(name)&limit=1" `shouldRespondWith`
[json|[ { "title": "The Catcher in the Rye", "author": { "name": "J.D. Salinger" } } ]|]
{ matchHeaders = [matchContentTypeJson] }
get "/sixties_books?select=title,author(name)&limit=1" `shouldRespondWith`
[json|[ { "title": "To Kill a Mockingbird", "author": { "name": "Harper Lee" } } ]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting children with composite key" $
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments(content)" `shouldRespondWith`
[json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $
get "/articles?id=eq.1&select=id,articleStars(users(*))" `shouldRespondWith`
[json|[{"id":1,"articleStars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed by FK column name" $
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"}}]|]
@@ -400,8 +329,109 @@ spec = do
[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 detect fk relations through views to tables in the public schema" $
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
describe "view embedding" $ do
it "can detect fk relations through views to tables in the public schema" $
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
it "can request parent without specifying primary key" $
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 detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $
get "/articles?id=eq.1&select=id,articleStars(users(*))" `shouldRespondWith`
[json|[{"id":1,"articleStars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works when requesting parents and children on views" $
get "/projects_view?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works when requesting parents and children on views with renamed keys" $
get "/projects_view_alt?t_id=eq.1&select=t_id, name, clients(*), tasks(id, name)" `shouldRespondWith`
[json|[{"t_id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "detects parent relations when having many views of a private table" $ do
get "/books?select=title,author(name)&id=eq.5" `shouldRespondWith`
[json|[ { "title": "Farenheit 451", "author": { "name": "Ray Bradbury" } } ]|]
{ matchHeaders = [matchContentTypeJson] }
get "/forties_books?select=title,author(name)&limit=1" `shouldRespondWith`
[json|[ { "title": "1984", "author": { "name": "George Orwell" } } ]|]
{ matchHeaders = [matchContentTypeJson] }
get "/fifties_books?select=title,author(name)&limit=1" `shouldRespondWith`
[json|[ { "title": "The Catcher in the Rye", "author": { "name": "J.D. Salinger" } } ]|]
{ matchHeaders = [matchContentTypeJson] }
get "/sixties_books?select=title,author(name)&limit=1" `shouldRespondWith`
[json|[ { "title": "To Kill a Mockingbird", "author": { "name": "Harper Lee" } } ]|]
{ matchHeaders = [matchContentTypeJson] }
it "works with views that have subselects" $
get "/authors_books_number?select=*,books(title)&id=eq.1" `shouldRespondWith`
[json|[ {"id":1, "name":"George Orwell","num_in_forties":1,"num_in_fifties":0,"num_in_sixties":0,"num_in_all_decades":1,
"books":[{"title":"1984"}]} ]|]
{ matchHeaders = [matchContentTypeJson] }
it "works with views that have case subselects" $
get "/authors_have_book_in_decade?select=*,books(title)&id=eq.3" `shouldRespondWith`
[json|[ {"id":3,"name":"Antoine de Saint-Exupéry","has_book_in_forties":true,"has_book_in_fifties":false,"has_book_in_sixties":false,
"books":[{"title":"The Little Prince"}]} ]|]
{ matchHeaders = [matchContentTypeJson] }
it "works with views that have subselect in the FROM clause" $
get "/forties_and_fifties_books?select=title,first_publisher,author:authors(name)&id=eq.1" `shouldRespondWith`
[json|[{"title":"1984","first_publisher":"Secker & Warburg","author":{"name":"George Orwell"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works with views that have CTE" $
get "/odd_years_publications?select=title,publication_year,first_publisher,author:authors(name)&id=in.(1,2,3)" `shouldRespondWith`
[json|[
{"title":"1984","publication_year":1949,"first_publisher":"Secker & Warburg","author":{"name":"George Orwell"}},
{"title":"The Diary of a Young Girl","publication_year":1947,"first_publisher":"Contact Publishing","author":{"name":"Anne Frank"}},
{"title":"The Little Prince","publication_year":1947,"first_publisher":"Reynal & Hitchcock","author":{"name":"Antoine de Saint-Exupéry"}} ]|]
{ matchHeaders = [matchContentTypeJson] }
it "works when having a capitalized table name and camelCase fk column" $
get "/foos?select=*,bars(*)" `shouldRespondWith` 200
it "works when embedding a view with a table that has a long compound pk" $ do
get "/player_view?select=id,contract(purchase_price)&id=in.(1,3,5,7)" `shouldRespondWith`
[json|
[{"id":1,"contract":[{"purchase_price":10}]},
{"id":3,"contract":[{"purchase_price":30}]},
{"id":5,"contract":[{"purchase_price":50}]},
{"id":7,"contract":[]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/contract?select=tournament,player_view(first_name)&limit=3" `shouldRespondWith`
[json|
[{"tournament":"tournament_1","player_view":{"first_name":"first_name_1"}},
{"tournament":"tournament_2","player_view":{"first_name":"first_name_2"}},
{"tournament":"tournament_3","player_view":{"first_name":"first_name_3"}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "works when embedding a view with a view that referes to a table that has a long compound pk" $ do
get "/player_view?select=id,contract_view(purchase_price)&id=in.(1,3,5,7)" `shouldRespondWith`
[json|
[{"id":1,"contract_view":[{"purchase_price":10}]},
{"id":3,"contract_view":[{"purchase_price":30}]},
{"id":5,"contract_view":[{"purchase_price":50}]},
{"id":7,"contract_view":[]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/contract_view?select=tournament,player_view(first_name)&limit=3" `shouldRespondWith`
[json|
[{"tournament":"tournament_1","player_view":{"first_name":"first_name_1"}},
{"tournament":"tournament_2","player_view":{"first_name":"first_name_2"}},
{"tournament":"tournament_3","player_view":{"first_name":"first_name_3"}}] |]
{ matchHeaders = [matchContentTypeJson] }
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`
[json|
[{"number_of_projects":1,"client":null},
{"number_of_projects":2,"client":{"name":"Microsoft"}},
{"number_of_projects":2,"client":{"name":"Apple"}}] |]
{ matchHeaders = [matchContentTypeJson] }
describe "path fixed" $ do
it "works when requesting children 2 levels" $
@@ -409,13 +439,7 @@ spec = do
[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" $ do
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 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"}},
@@ -423,6 +447,14 @@ spec = do
{"id":3,"body":"How are you doing?","sender":{"name":"John"},"recipient":{"name":"Jane"}}] |]
{ 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":[]}]|]
@@ -630,16 +662,6 @@ spec = do
, matchHeaders = ["Content-Range" <:> "0-1/*"]
}
it "by a json column property asc" $
get "/json?order=data->>id.asc" `shouldRespondWith`
[json| [{"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}, {"data": {"id": 3}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "by a json column with two level property nulls first" $
get "/json?order=data->foo->>bar.nullsfirst" `shouldRespondWith`
[json| [{"data": {"id": 3}}, {"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "without other constraints" $
get "/items?order=id.asc" `shouldRespondWith` 200
@@ -771,22 +793,6 @@ spec = do
respHeaders `shouldSatisfy` matchHeader
"Content-Location" "/simple_pk"
describe "jsonb" $ do
it "can filter by properties inside json column" $ do
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
[json| [] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter by properties inside json column using not" $
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
[json| [] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter by properties inside json column using ->>" $
get "/json?data->>id=eq.1" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
describe "weird requests" $ do
it "can query as normal" $ do
@@ -813,6 +819,20 @@ spec = do
[json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]
{ matchHeaders = [matchContentTypeJson] }
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`
[json|[
{"Just A Server Model":" IBM,9113-550 (P5-550)"},
{"Just A Server Model":" IBM,9113-550 (P5-550)"},
{"Just A Server Model":" IBM,9131-52A (P5-52A)"},
{"Just A Server Model":" IBM,9133-55A (P5-55A)"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "will select and filter a quoted column that has PostgREST reserved characters" $
get "/pgrst_reserved_chars?select=%22:arr-%3Eow::cast%22,%22(inside,parens)%22,%22a.dotted.column%22,%22%20%20col%20%20w%20%20space%20%20%22&%22*id*%22=eq.1" `shouldRespondWith`
[json|[{":arr->ow::cast":" arrow-1 ","(inside,parens)":" parens-1 ","a.dotted.column":" dotted-1 "," col w space ":" space-1"}]|]
{ matchHeaders = [matchContentTypeJson] }
describe "binary output" $ do
context "on GET" $ do
it "can query if a single column is selected" $
@@ -995,3 +1015,6 @@ spec = do
get "/projects_dump?select=id,name&order=id.desc&limit=3" `shouldRespondWith`
[json| [{"id":5,"name":"Orphan"}, {"id":4,"name":"OSX"}, {"id":3,"name":"IOS"}] |]
{ matchHeaders = [matchContentTypeJson] }
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
+16 -1
View File
@@ -1,6 +1,6 @@
module Feature.RpcSpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
@@ -78,6 +78,16 @@ spec =
get "/rpc/sayhello" `shouldRespondWith` 404
get "/rpc/sayhello?any_arg=value" `shouldRespondWith` 404
it "works when having uppercase identifiers" $ do
get "/rpc/quotedFunction?user=mscott&fullName=Michael Scott&SSN=401-32-XXXX" `shouldRespondWith`
[json|{"user": "mscott", "fullName": "Michael Scott", "SSN": "401-32-XXXX"}|]
{ matchHeaders = [matchContentTypeJson] }
post "/rpc/quotedFunction"
[json|{"user": "dschrute", "fullName": "Dwight Schrute", "SSN": "030-18-XXXX"}|]
`shouldRespondWith`
[json|{"user": "dschrute", "fullName": "Dwight Schrute", "SSN": "030-18-XXXX"}|]
{ matchHeaders = [matchContentTypeJson] }
context "shaping the response returned by a proc" $ do
it "returns a project" $ do
post "/rpc/getproject" [json| { "id": 1} |] `shouldRespondWith`
@@ -361,3 +371,8 @@ spec =
get "/rpc/get_tsearch?text_search_vector=not.fts(english).fun%7Crat" `shouldRespondWith`
[json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "should work with an argument of custom type in public schema" $
get "/rpc/test_arg?my_arg=something" `shouldRespondWith`
[json|"foobar"|]
{ matchHeaders = [matchContentTypeJson] }
+72 -8
View File
@@ -1,6 +1,6 @@
module Feature.StructureSpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai
import Network.HTTP.Types
@@ -85,6 +85,27 @@ spec = do
deleteResponse `shouldBe` Just "No Content"
it "includes an array type for GET responses" $ do
r <- simpleBody <$> get "/"
let childGetSchema = r ^? key "paths"
. key "/child_entities"
. key "get"
. key "responses"
. key "200"
. key "schema"
liftIO $
childGetSchema `shouldBe` Just
[aesonQQ|
{
"items": {
"$ref": "#/definitions/child_entities"
},
"type": "array"
}
|]
it "includes definitions to tables" $ do
r <- simpleBody <$> get "/"
@@ -113,7 +134,10 @@ spec = do
"format": "integer",
"type": "integer"
}
}
},
"required": [
"id"
]
}
|]
@@ -163,15 +187,54 @@ spec = do
]
|]
describe "Materialized view" $
it "includes materialized view properties" $ do
r <- simpleBody <$> get "/"
let method s = key "paths" . key "/materialized_projects" . key s
summary = r ^? method "get" . key "summary"
description = r ^? method "get" . key "description"
parameters = r ^? method "get" . key "parameters"
liftIO $ do
summary `shouldBe` Just "A materialized view for projects"
description `shouldBe` Just "Just a test for materialized views"
parameters `shouldBe` Just
[aesonQQ|
[
{ "$ref": "#/parameters/rowFilter.materialized_projects.id" },
{ "$ref": "#/parameters/rowFilter.materialized_projects.name" },
{ "$ref": "#/parameters/rowFilter.materialized_projects.client_id" },
{ "$ref": "#/parameters/select" },
{ "$ref": "#/parameters/order" },
{ "$ref": "#/parameters/range" },
{ "$ref": "#/parameters/rangeUnit" },
{ "$ref": "#/parameters/offset" },
{ "$ref": "#/parameters/limit" },
{ "$ref": "#/parameters/preferCount" }
]
|]
describe "RPC" $ do
it "includes body schema for arguments" $ do
it "includes function summary/description and body schema for arguments" $ do
r <- simpleBody <$> get "/"
let args = r ^? key "paths" . key "/rpc/varied_arguments"
. key "post" . key "parameters"
. nth 0 . key "schema"
liftIO $
let method s = key "paths" . key "/rpc/varied_arguments" . key s
args = r ^? method "post" . key "parameters" . nth 0 . key "schema"
summary = r ^? method "post" . key "summary"
description = r ^? method "post" . key "description"
liftIO $ do
summary `shouldBe` Just "An RPC function"
description `shouldBe` Just "Just a test for RPC function arguments"
args `shouldBe` Just
[aesonQQ|
{
@@ -213,7 +276,8 @@ spec = do
"type": "integer"
}
},
"type": "object"
"type": "object",
"description": "An RPC function\n\nJust a test for RPC function arguments"
}
|]
+27
View File
@@ -198,3 +198,30 @@ spec =
[("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]
[str| [ { "name": "Ruby", "rank": 11 } ]|]
`shouldRespondWith` [json|{ "name": "Ruby", "rank": 11 }|] { matchHeaders = [matchContentTypeSingular] }
context "with a camel case pk column" $ do
it "works with POST and merge-duplicates/ignore-duplicates headers" $ do
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json| [
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
]|] `shouldRespondWith` [json|[
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
}
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json| [
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
]|] `shouldRespondWith` [json|[]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
}
it "works with PUT" $ do
put "/UnitTest?idUnitTest=eq.1" [str| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] `shouldRespondWith` 204
get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith`
[json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] { matchHeaders = [matchContentTypeJson] }
+31 -15
View File
@@ -4,11 +4,11 @@ import Test.Hspec
import SpecHelper
import qualified Hasql.Pool as P
import qualified Hasql.Transaction.Sessions as HT
import PostgREST.App (postgrest)
import PostgREST.Config (pgVersion95, pgVersion96, configSettings)
import PostgREST.DbStructure (getDbStructure, getPgVersion, fillSessionWithSettings)
import PostgREST.Types (DbStructure(..))
import PostgREST.DbStructure (getDbStructure, getPgVersion)
import PostgREST.Types (DbStructure(..), pgVersion95, pgVersion96)
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, updateAction)
import Data.Function (id)
import Data.IORef
@@ -21,7 +21,9 @@ import qualified Feature.AudienceJwtSecretSpec
import qualified Feature.ConcurrentSpec
import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec
import qualified Feature.ExtraSearchPathSpec
import qualified Feature.InsertSpec
import qualified Feature.JsonOperatorSpec
import qualified Feature.NoJwtSpec
import qualified Feature.QueryLimitedSpec
import qualified Feature.QuerySpec
@@ -33,6 +35,7 @@ import qualified Feature.ProxySpec
import qualified Feature.AndOrParamsSpec
import qualified Feature.RpcSpec
import qualified Feature.NonexistentSchemaSpec
import qualified Feature.PgVersion95Spec
import qualified Feature.PgVersion96Spec
import qualified Feature.UpsertSpec
@@ -45,7 +48,9 @@ main = do
pool <- P.acquire (3, 10, toS testDbConn)
result <- P.use pool $ getDbStructure "test" =<< getPgVersion
result <- P.use pool $ do
ver <- getPgVersion
HT.transaction HT.ReadCommitted HT.Read $ getDbStructure "test" ver
dbStructure <- pure $ either (panic.show) id result
@@ -53,22 +58,25 @@ main = do
refDbStructure <- newIORef $ Just dbStructure
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure ()
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure ()
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure ()
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure ()
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure ()
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure ()
audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool getTime $ pure ()
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool getTime $ pure ()
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure ()
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure ()
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure ()
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure ()
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure ()
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure ()
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure ()
audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool getTime $ pure ()
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool getTime $ pure ()
asymJwkSetApp = return $ postgrest (testCfgAsymJWKSet testDbConn) refDbStructure pool getTime $ pure ()
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure ()
extraSearchPathApp = return $ postgrest (testCfgExtraSearchPath testDbConn) refDbStructure pool getTime $ pure ()
let reset :: IO ()
reset = P.use pool (fillSessionWithSettings (configSettings $ testCfg testDbConn)) >> resetDb testDbConn
reset = resetDb testDbConn
actualPgVersion = pgVersion dbStructure
extraSpecs =
[("Feature.UpsertSpec", Feature.UpsertSpec.spec) | actualPgVersion >= pgVersion95] ++
[("Feature.PgVersion95Spec", Feature.PgVersion95Spec.spec) | actualPgVersion >= pgVersion95] ++
[("Feature.PgVersion96Spec", Feature.PgVersion96Spec.spec) | actualPgVersion >= pgVersion96]
specs = uncurry describe <$> [
@@ -77,13 +85,13 @@ main = do
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
, ("Feature.InsertSpec" , Feature.InsertSpec.spec)
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec)
, ("Feature.QuerySpec" , Feature.QuerySpec.spec)
, ("Feature.RpcSpec" , Feature.RpcSpec.spec)
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec)
, ("Feature.NonexistentSchemaSpec" , Feature.NonexistentSchemaSpec.spec)
] ++ extraSpecs
hspec $ do
@@ -117,6 +125,14 @@ main = do
beforeAll_ reset . before asymJwkApp $
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
-- this test runs with asymmetric JWKSet
beforeAll_ reset . before asymJwkSetApp $
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
-- this test runs with a nonexistent db-schema
beforeAll_ reset . before nonexistentSchemaApp $
describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec
-- this test runs with an extra search path
beforeAll_ reset . before extraSearchPathApp $
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
+15 -5
View File
@@ -19,7 +19,7 @@ import Text.Heredoc
import PostgREST.Config (AppConfig(..))
import PostgREST.Types (JSPathExp(..))
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai
import Network.HTTP.Types
@@ -61,9 +61,8 @@ validateOpenApiResponse headers = do
D4.fetchFilesystemAndValidate schemaContext ((fromJust . decode) respBody) `shouldReturn` Right ()
getEnvVarWithDefault :: Text -> Text -> IO Text
getEnvVarWithDefault var def = do
varValue <- getEnv (toS var) `E.catchIOError` const (return $ toS def)
return $ toS varValue
getEnvVarWithDefault var def = toS <$>
getEnv (toS var) `E.catchIOError` const (return $ toS def)
_baseCfg :: AppConfig
_baseCfg = -- Connection Settings
@@ -79,6 +78,8 @@ _baseCfg = -- Connection Settings
]
-- Default role claim key
(Right [JSPKey "role"])
-- Empty db-extra-search-path
[]
testCfg :: Text -> AppConfig
testCfg testDbConn = _baseCfg { configDatabase = testDbConn }
@@ -114,9 +115,18 @@ testCfgAsymJWK testDbConn = (testCfg testDbConn) {
[str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|]
}
testCfgAsymJWKSet :: Text -> AppConfig
testCfgAsymJWKSet testDbConn = (testCfg testDbConn) {
configJwtSecret = Just $ encodeUtf8
[str|{"keys": [{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}]}|]
}
testNonexistentSchemaCfg :: Text -> AppConfig
testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configSchema = "nonexistent" }
testCfgExtraSearchPath :: Text -> AppConfig
testCfgExtraSearchPath testDbConn = (testCfg testDbConn) { configExtraSearchPath = ["public", "extensions"] }
setupDb :: Text -> IO ()
setupDb dbConn = do
loadFixture dbConn "database"
@@ -166,5 +176,5 @@ isErrorFormat s =
S.null (S.difference keys validKeys)
where
obj = decode s :: Maybe (M.Map Text Value)
keys = fromMaybe S.empty (M.keysSet <$> obj)
keys = maybe S.empty M.keysSet obj
validKeys = S.fromList ["message", "details", "hint", "code"]
+1 -1
View File
@@ -1,4 +1,4 @@
#! /bin/bash
#! /usr/bin/env bash
if [ -z "$1" ]
then
echo "Please supply the connection uri for the user with create database privileges"
+1 -1
View File
@@ -1,4 +1,4 @@
#! /bin/bash
#! /usr/bin/env bash
if [ -z "$1" ]
then
echo "Please supply the connection uri for the user with create database privileges"
+88 -10
View File
@@ -250,7 +250,7 @@ INSERT INTO tsearch VALUES (to_tsvector('It''s kind of fun to do the impossible'
INSERT INTO tsearch VALUES (to_tsvector('But also fun to do what is possible'));
INSERT INTO tsearch VALUES (to_tsvector('Fat cats ate rats'));
INSERT INTO tsearch VALUES (to_tsvector('french', 'C''est un peu amusant de faire l''impossible'));
INSERT INTO tsearch VALUES (to_tsvector('german', 'Es ist eine Art Spaß, das Unmögliche zu machen'));
INSERT INTO tsearch VALUES (to_tsvector('german', 'Es ist eine Art Spaß, das Unmögliche zu machen'));
--
-- Data for Name: users_projects; Type: TABLE DATA; Schema: test; Owner: -
@@ -373,16 +373,27 @@ INSERT INTO authors VALUES (7, 'Harper Lee');
INSERT INTO authors VALUES (8, 'Kurt Vonnegut');
INSERT INTO authors VALUES (9, 'Ken Kesey');
TRUNCATE TABLE publishers CASCADE;
INSERT INTO publishers VALUES (1, 'Secker & Warburg');
INSERT INTO publishers VALUES (2, 'Contact Publishing');
INSERT INTO publishers VALUES (3, 'Reynal & Hitchcock');
INSERT INTO publishers VALUES (4, 'Little, Brown and Company');
INSERT INTO publishers VALUES (5, 'Ballantine Books');
INSERT INTO publishers VALUES (6, 'Faber and Faber');
INSERT INTO publishers VALUES (7, 'J. B. Lippincott & Co.');
INSERT INTO publishers VALUES (8, 'Delacorte');
INSERT INTO publishers VALUES (9, 'Viking Press & Signet Books');
TRUNCATE TABLE books CASCADE;
INSERT INTO books VALUES (1, '1984', 1949, 1);
INSERT INTO books VALUES (2, 'The Diary of a Young Girl', 1947, 2);
INSERT INTO books VALUES (3, 'The Little Prince', 1947, 3);
INSERT INTO books VALUES (4, 'The Catcher in the Rye', 1951, 4);
INSERT INTO books VALUES (5, 'Farenheit 451', 1953, 5);
INSERT INTO books VALUES (6, 'Lord of the Flies', 1954, 6);
INSERT INTO books VALUES (7, 'To Kill a Mockingbird', 1960, 7);
INSERT INTO books VALUES (8, 'Slaughterhouse-Five', 1969, 8);
INSERT INTO books VALUES (9, 'One Flew Over the Cuckoo''s Nest', 1962, 9);
INSERT INTO books VALUES (1, '1984', 1949, 1, 1);
INSERT INTO books VALUES (2, 'The Diary of a Young Girl', 1947, 2, 2);
INSERT INTO books VALUES (3, 'The Little Prince', 1947, 3, 3);
INSERT INTO books VALUES (4, 'The Catcher in the Rye', 1951, 4, 4);
INSERT INTO books VALUES (5, 'Farenheit 451', 1953, 5, 5);
INSERT INTO books VALUES (6, 'Lord of the Flies', 1954, 6, 6);
INSERT INTO books VALUES (7, 'To Kill a Mockingbird', 1960, 7, 7);
INSERT INTO books VALUES (8, 'Slaughterhouse-Five', 1969, 8, 8);
INSERT INTO books VALUES (9, 'One Flew Over the Cuckoo''s Nest', 1962, 9, 9);
SET search_path = test, pg_catalog;
@@ -411,3 +422,70 @@ INSERT INTO zone VALUES (4, 'store 4', 3, 1);
-- for foreign table projects_dump
copy (select id, name, client_id from projects) to '/tmp/projects_dump.csv' with csv;
TRUNCATE TABLE "UnitTest" CASCADE;
INSERT INTO "UnitTest" VALUES (1, 'unit test 1');
TRUNCATE TABLE json_arr CASCADE;
INSERT INTO json_arr VALUES (1, '[1, 2, 3]');
INSERT INTO json_arr VALUES (2, '[4, 5, 6]');
INSERT INTO json_arr VALUES (3, '[[9, 8, 7], [11, 12, 13]]');
INSERT INTO json_arr VALUES (4, '[[[5, 6], 7, 8]]');
INSERT INTO json_arr VALUES (5, '[{"a": "A"}, {"b": "B"}]');
INSERT INTO json_arr VALUES (6, '[{"a": [1,2,3]}, {"b": [4,5]}]');
INSERT INTO json_arr VALUES (7, '{"c": [1,2,3], "d": [4,5]}');
INSERT INTO json_arr VALUES (8, '{"c": [{"d": [4,5,6,7,8]}]}');
INSERT INTO json_arr VALUES (9, '[{"0xy1": [1,{"23-xy-45": [2, {"xy-6": [3]}]}]}]');
TRUNCATE TABLE jsonb_test CASCADE;
INSERT INTO jsonb_test VALUES (1, '{ "a": {"b": 2} }');
INSERT INTO jsonb_test VALUES (2, '{ "c": [1,2,3] }');
INSERT INTO jsonb_test VALUES (3, '[{ "d": "test" }]');
INSERT INTO jsonb_test VALUES (4, '{ "e": 1 }');
TRUNCATE TABLE private.player CASCADE;
INSERT into private.player
SELECT
generate_series,
'first_name_' || generate_series,
'last_name_' || generate_series,
'2018-10-11'
FROM generate_series(1, 12);
TRUNCATE TABLE contract CASCADE;
insert into contract
select
'tournament_' || generate_series,
tsrange(now()::timestamp, null),
10*generate_series,
generate_series,
'first_name_' || generate_series,
'last_name_' || generate_series,
'2018-10-11'
from generate_series(1, 6);
TRUNCATE TABLE ltree_sample CASCADE;
INSERT INTO ltree_sample VALUES ('Top');
INSERT INTO ltree_sample VALUES ('Top.Science');
INSERT INTO ltree_sample VALUES ('Top.Science.Astronomy');
INSERT INTO ltree_sample VALUES ('Top.Science.Astronomy.Astrophysics');
INSERT INTO ltree_sample VALUES ('Top.Science.Astronomy.Cosmology');
TRUNCATE TABLE isn_sample CASCADE;
INSERT INTO isn_sample VALUES ('978-0-393-04002-9', 'Mathematics: From the Birth of Numbers');
TRUNCATE TABLE "Server Today" CASCADE;
COPY "Server Today" ("cHostname", "Just A Server Model") FROM STDIN CSV DELIMITER '|';
argnim1 | IBM,9113-550 (P5-550)
argnim2 | IBM,9113-550 (P5-550)
daaa2nim71 | IBM,9131-52A (P5-52A)
daah3nim71 | IBM,8406-71Y (P7-PS701)
hbnim1 | IBM,9133-55A (P5-55A)
\.
TRUNCATE TABLE pgrst_reserved_chars CASCADE;
COPY pgrst_reserved_chars ("*id*", ":arr->ow::cast", "(inside,parens)", "a.dotted.column", " col w space ") FROM STDIN CSV DELIMITER '|';
1 | arrow-1 | parens-1 | dotted-1 | space-1
2 | arrow-2 | parens-2 | dotted-2 | space-2
3 | arrow-3 | parens-3 | dotted-3 | space-3
\.
+1 -1
View File
@@ -1,3 +1,3 @@
set client_min_messages to warning;
DROP SCHEMA IF EXISTS test, private, postgrest, jwt, public, تست CASCADE;
DROP SCHEMA IF EXISTS test, private, postgrest, jwt, public, تست, extensions CASCADE;
DROP TYPE IF EXISTS jwt_token CASCADE;
+19
View File
@@ -5,6 +5,7 @@ GRANT USAGE ON SCHEMA
, jwt
, public
, "تست"
, extensions
TO postgrest_test_anonymous;
-- Schema test objects
@@ -79,6 +80,24 @@ GRANT ALL ON TABLE
, space
, zone
, projects_dump
, "UnitTest"
, json_arr
, jsonb_test
, authors_books_number
, authors_have_book_in_decade
, forties_and_fifties_books
, odd_years_publications
, foos
, bars
, materialized_projects
, contract
, player_view
, contract_view
, ltree_sample
, isn_sample
, projects_count_grouped_by
, "Server Today"
, pgrst_reserved_chars
TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
+213 -15
View File
@@ -17,7 +17,7 @@ CREATE SCHEMA postgrest;
CREATE SCHEMA private;
CREATE SCHEMA test;
CREATE SCHEMA تست;
CREATE SCHEMA extensions;
--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
@@ -222,6 +222,10 @@ AS $_$
SELECT 'Hi'::text;
$_$;
COMMENT ON FUNCTION varied_arguments(double precision, character varying, boolean, date, money, enum_menagerie_type, integer) IS
$_$An RPC function
Just a test for RPC function arguments$_$;
--
-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -
@@ -1120,17 +1124,17 @@ create view images_base64 as (
select name, replace(encode(img, 'base64'), E'\n', '') as img from images
);
create function test.ret_enum(val text) returns test.enum_menagerie_type as $$
create function test.ret_enum(val text) returns test.enum_menagerie_type as $$
select val::test.enum_menagerie_type;
$$ language sql;
create domain one_nine as integer check (value >= 1 and value <= 9);
create function test.ret_array() returns integer[] as $$
create function test.ret_array() returns integer[] as $$
select '{1,2,3}'::integer[];
$$ language sql;
create function test.ret_domain(val integer) returns test.one_nine as $$
create function test.ret_domain(val integer) returns test.one_nine as $$
select val::test.one_nine;
$$ language sql;
@@ -1144,20 +1148,20 @@ $$ language sql;
create function test.ret_scalars() returns table(
a text, b test.enum_menagerie_type, c test.one_nine, d int4range
) as $$
select row('scalars'::text, enum_first(null::test.enum_menagerie_type),
) as $$
select row('scalars'::text, enum_first(null::test.enum_menagerie_type),
1::test.one_nine, int4range(10, 20));
$$ language sql;
create type test.point_2d as (x integer, y integer);
create function test.ret_point_2d() returns test.point_2d as $$
create function test.ret_point_2d() returns test.point_2d as $$
select row(10, 5)::test.point_2d;
$$ language sql;
create type private.point_3d as (x integer, y integer, z integer);
create function test.ret_point_3d() returns private.point_3d as $$
create function test.ret_point_3d() returns private.point_3d as $$
select row(7, -3, 4)::private.point_3d;
$$ language sql;
@@ -1171,17 +1175,17 @@ create function test.ret_rows_with_base64_bin() returns setof test.images_base64
select i.name, i.img from test.images_base64 i;
$$ language sql;
create function test.single_article(id integer) returns test.articles as $$
create function test.single_article(id integer) returns test.articles as $$
select a.* from test.articles a where a.id = $1;
$$ language sql;
create function test.get_guc_value(name text) returns text as $$
create function test.get_guc_value(name text) returns text as $$
select nullif(current_setting(name), '')::text;
$$ language sql;
create table w_or_wo_comma_names ( name text );
create table items_with_different_col_types (
create table items_with_different_col_types (
int_data integer,
text_data text,
bool_data bool,
@@ -1194,20 +1198,20 @@ create table items_with_different_col_types (
-- Tables used for testing complex boolean logic with and/or query params
create table entities (
create table entities (
id integer primary key,
name text,
arr integer[],
text_search_vector tsvector
);
create table child_entities (
create table child_entities (
id integer primary key,
name text,
parent_id integer references entities(id)
);
create table grandchild_entities (
create table grandchild_entities (
id integer primary key,
name text,
parent_id integer references child_entities(id),
@@ -1396,11 +1400,17 @@ create table private.authors(
name text
);
create table private.publishers(
id integer primary key,
name text
);
create table private.books(
id integer primary key,
title text,
publication_year smallint,
author_id integer references private.authors(id)
author_id integer references private.authors(id),
first_publisher_id integer references private.publishers(id)
);
create view test.authors as select id, name from private.authors;
@@ -1451,3 +1461,191 @@ comment on foreign table projects_dump is
$$A temporary projects dump
Just a test for foreign tables$$;
create table "UnitTest"(
"idUnitTest" integer primary key,
"nameUnitTest" text
);
create table json_arr(
id integer primary key,
data pg_catalog.json
);
create table jsonb_test(
id integer primary key,
data jsonb
);
create view test.authors_books_number as
select
id,
name,
(
select
count(*)
from forties_books where author_id = authors.id
) as num_in_forties,
(
select
count(*)
from fifties_books where author_id = authors.id
) as num_in_fifties,
(
select
count(*)
from sixties_books where author_id = authors.id
) as num_in_sixties,
(
select
count(*)
from (
select id
from forties_books where author_id = authors.id
union
select id
from fifties_books where author_id = authors.id
union
select id
from sixties_books where author_id = authors.id
) _
) as num_in_all_decades
from private.authors;
create view test.authors_have_book_in_decade as
select
id,
name,
case
when (x.id in (select author_id from test.forties_books))
then true
else false
end as has_book_in_forties,
case
when (x.id in (select author_id from test.fifties_books))
then true
else false
end as has_book_in_fifties,
case
when (x.id in (select author_id from test.sixties_books))
then true
else false
end as has_book_in_sixties
from private.authors x;
create view test.forties_and_fifties_books as
select x.id, x.title, x.publication_year, y.name as first_publisher, x.author_id
from (
select id, title, publication_year, author_id, first_publisher_id from private.books
where publication_year >= 1940 and publication_year < 1960) x
join private.publishers y on y.id = x.first_publisher_id;
create view test.odd_years_publications as
with
odd_years_books as(
select id, title, publication_year, author_id, first_publisher_id
from private.books
where publication_year % 2 <> 0
)
select
x.id, x.title, x.publication_year,
y.name as first_publisher, x.author_id
from odd_years_books x
join private.publishers y on y.id = x.first_publisher_id;
create view test.projects_count_grouped_by as
select
client_id,
count(id) as number_of_projects
from projects
group by client_id;
CREATE TABLE test."Foo"(
id int primary key,
name text
);
CREATE TABLE test.bar(
id int primary key,
name text,
"fooId" int references "Foo"(id)
);
CREATE VIEW test.foos as select id,name from "Foo";
CREATE VIEW test.bars as select id, "fooId", name from bar;
create materialized view materialized_projects as
select id, name, client_id from projects;
comment on materialized view materialized_projects is
$$A materialized view for projects
Just a test for materialized views$$;
create or replace function test."quotedFunction"("user" text, "fullName" text, "SSN" text)
returns jsonb AS $$
select format('{"user": "%s", "fullName": "%s", "SSN": "%s"}', "user", "fullName", "SSN")::jsonb;
$$ language sql;
create table private.player (
id integer not null,
first_name text not null,
last_name text not null,
birth_date date,
primary key (last_name, id, first_name, birth_date) -- just for testing a long compound pk
);
create table test.contract (
tournament text not null,
time tsrange not null,
purchase_price int not null,
id integer not null,
first_name text not null,
last_name text not null,
birth_date date,
foreign key (last_name, id, first_name, birth_date) references private.player
);
create view test.player_view as select * from private.player;
create view test.contract_view as select * from test.contract;
create type public.my_type AS enum ('something');
create function test.test_arg(my_arg public.my_type) returns text as $$
select 'foobar'::text;
$$ language sql;
create extension if not exists ltree with schema public;
create table test.ltree_sample (
path public.ltree
);
CREATE FUNCTION test.number_of_labels(test.ltree_sample) RETURNS integer AS $$
SELECT nlevel($1.path)
$$ language sql;
create extension if not exists isn with schema extensions;
create table test.isn_sample (
id extensions.isbn,
name text
);
create function test.is_valid_isbn(input text) returns boolean as $$
select is_valid(input::isbn);
$$ language sql;
create table "Server Today"(
"cHostname" text,
"Just A Server Model" text
);
create table test.pgrst_reserved_chars (
"*id*" integer,
":arr->ow::cast" text,
"(inside,parens)" text,
"a.dotted.column" text,
" col w space " text
);
+67
View File
@@ -82,6 +82,24 @@ readSecretFromFile(){
pgrStop
}
readDbUriFromFile(){
pgrConfig="dburi-from-file.config"
pgrStartRead "./configs/$pgrConfig" "./dburis/$1"
while pgrStarted && test "$( rootStatus )" -ne 200
do
# wait for the server to start
sleep 0.1 \
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
done
if pgrStarted
then
ok "connection with $2 dburi read from a file"
else
ko "failed to read $2 dburi from a file"
fi
pgrStop
}
reqWithRoleClaimKey(){
export ROLE_CLAIM_KEY=$1
pgrStart "./configs/role-claim-key.config"
@@ -120,6 +138,49 @@ invalidRoleClaimKey(){
pgrStop
}
# ensure iat claim is successful in the presence of pgrst time cache, see https://github.com/PostgREST/postgrest/issues/1139
ensureIatClaimWorks(){
pgrStart "./configs/simple.config"
while pgrStarted && test "$( rootStatus )" -ne 200
do
# wait for the server to start
sleep 0.1 \
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
done
for i in {1..10}; do \
iatJwt=$(psql -qtAX postgrest_test -c "select jwt.sign(row_to_json(r), 'reallyreallyreallyreallyverysafe') from ( select 'postgrest_test_author' as role, extract(epoch from now()) as iat) r")
httpStatus="$( authorsStatus $iatJwt )"
if test "$httpStatus" -ne 200
then
ko "iat claim rejected with $httpStatus"
return
fi
sleep .5;\
done
ok "accepted iat claim"
pgrStop
}
# ensure app settings don't reset on pool timeout of 10 seconds, see https://github.com/PostgREST/postgrest/issues/1141
ensureAppSettings(){
pgrStart "./configs/app-settings.config"
while pgrStarted && test "$( rootStatus )" -ne 200
do
# wait for the server to start
sleep 0.1 \
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
done
sleep 11
response=$(curl -s "http://localhost:$pgrPort/rpc/get_guc_value?name=app.settings.external_api_secret")
if test "$response" = "\"0123456789abcdef\""
then
ok "GET /rpc/get_guc_value response is $response"
else
ko "GET /rpc/get_guc_value response was $response"
fi
pgrStop
}
# PRE: curl must be available
test -n "$(command -v curl)" || bailOut 'curl is not available'
@@ -144,6 +205,9 @@ readSecretFromFile ascii.b64 'Base64 (ASCII)'
readSecretFromFile utf8.b64 'Base64 (UTF-8)'
readSecretFromFile binary.b64 'Base64 (binary)'
readDbUriFromFile uri.noeol "(no EOL)"
readDbUriFromFile uri.txt "(EOL)"
reqWithRoleClaimKey '.postgrest.a_role' '{"postgrest":{"a_role":"postgrest_test_author"}}' 200
reqWithRoleClaimKey '.customObject.manyRoles[1]' '{"customObject":{"manyRoles": ["other", "postgrest_test_author"]}}' 200
reqWithRoleClaimKey '."https://www.example.com/roles"[0].value' '{"https://www.example.com/roles":[{"value":"postgrest_test_author"}]}' 200
@@ -157,6 +221,9 @@ invalidRoleClaimKey '.#$%&$%/'
invalidRoleClaimKey ''
invalidRoleClaimKey 1234
ensureIatClaimWorks
ensureAppSettings
cleanUp
exit $failedTests
@@ -0,0 +1,8 @@
db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
app.settings.external_api_secret = "0123456789abcdef"
@@ -2,7 +2,7 @@ db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "*4"
server-host = "127.0.0.1"
server-port = 49421
# Read secret from a file: /dev/stdin (alias for standard input)
@@ -0,0 +1,7 @@
db-uri = "@/dev/stdin"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
jwt-secret = "reallyreallyreallyreallyverysafe"
+1 -1
View File
@@ -2,7 +2,7 @@ db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "*4"
server-host = "127.0.0.1"
server-port = 49421
role-claim-key = "$(ROLE_CLAIM_KEY)"
jwt-secret = "reallyreallyreallyreallyverysafe"
@@ -2,7 +2,7 @@ db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "*4"
server-host = "127.0.0.1"
server-port = 49421
# Read secret from a file: /dev/stdin (alias for standard input)
+7
View File
@@ -0,0 +1,7 @@
db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
jwt-secret = "reallyreallyreallyreallyverysafe"
+1
View File
@@ -0,0 +1 @@
postgres:///postgrest_test
+1
View File
@@ -0,0 +1 @@
postgres:///postgrest_test
+8 -8
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#! /usr/bin/env bash
currentTest=1
failedTests=0
result(){ echo "$1 $currentTest $2"; currentTest=$(( $currentTest + 1 )); }
@@ -96,20 +96,20 @@ setUp
echo "Running memory usage tests.."
jsonKeyTest "1M" "POST" "/rpc/leak" "15M"
jsonKeyTest "1M" "POST" "/leak" "15M"
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1" "15M"
jsonKeyTest "1M" "POST" "/rpc/leak" "20M"
jsonKeyTest "1M" "POST" "/leak" "20M"
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1" "20M"
jsonKeyTest "10M" "POST" "/rpc/leak" "105M"
jsonKeyTest "10M" "POST" "/leak" "105M"
jsonKeyTest "10M" "PATCH" "/leak?id=eq.1" "105M"
jsonKeyTest "100M" "POST" "/rpc/leak" "895M"
jsonKeyTest "100M" "POST" "/leak" "895M"
jsonKeyTest "100M" "PATCH" "/leak?id=eq.1" "895M"
jsonKeyTest "50M" "POST" "/rpc/leak" "500M"
jsonKeyTest "50M" "POST" "/leak" "500M"
jsonKeyTest "50M" "PATCH" "/leak?id=eq.1" "500M"
postJsonArrayTest "1000" "/perf_articles" "20M"
postJsonArrayTest "10000" "/perf_articles" "120M"
postJsonArrayTest "10000" "/perf_articles" "150M"
postJsonArrayTest "100000" "/perf_articles" "1.15G"
cleanUp
+1 -1
View File
@@ -2,7 +2,7 @@ db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "*4"
server-host = "127.0.0.1"
server-port = 49421
jwt-secret = "reallyreallyreallyreallyverysafe"