packages feed

postgrest 9.0.0 → 9.0.1

raw patch · 98 files changed

+8951/−8843 lines, 98 filesdep −ansi-wl-pprintdep −fast-loggerdep −wai-loggerdep ~protoludedep ~wai-extradep ~warp

Dependencies removed: ansi-wl-pprint, fast-logger, wai-logger, wai-middleware-static

Dependency ranges changed: protolude, wai-extra, warp

Files

CHANGELOG.md view
@@ -3,11 +3,25 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased--### Added+## [9.0.1] - 2022-06-03  ### Fixed++ - #2165, Fix json/jsonb columns should not have type in OpenAPI spec - @clrnd+ - #2020, Execute deferred constraint triggers when using `Prefer: tx=rollback` - @wolfgangwalther+ - #2077, Fix `is` not working with upper or mixed case values like `NULL, TrUe, FaLsE` - @steve-chavez+ - #2024, Fix schema cache loading when views with XMLTABLE and DEFAULT are present - @wolfgangwalther+ - #1724, Fix wrong CORS header Authentication -> Authorization - @wolfgangwalther+ - #2120, Fix reading database configuration properly when `=` is present in value - @wolfgangwalther+ - #2135, Remove trigger functions from schema cache and OpenAPI output, because they can't be called directly anyway. - @wolfgangwalther+ - #2101, Remove aggregates, procedures and window functions from the schema cache and OpenAPI output. - @wolfgangwalther+ - #2153, Fix --dump-schema running with a wrong PG version. - @wolfgangwalther+ - #2042, Keep working when EMFILE(Too many open files) is reached. - @steve-chavez+ - #2147, Ignore `Content-Type` headers for `GET` requests when calling RPCs. - @laurenceisla+   + Previously, `GET` without parameters, but with `Content-Type: text/plain` or `Content-Type: application/octet-stream` would fail with `404 Not Found`, even if a function without arguments was available.+ - #2239, Fix misleading disambiguation error where the content of the `relationship` key looks like valid syntax - @laurenceisla+ - #2294, Disable parallel GC for better performance on higher core CPUs - @steve-chavez+ - #1076, Fix using CPU while idle - @steve-chavez  ## [9.0.0] - 2021-11-25 
postgrest.cabal view
@@ -1,8 +1,8 @@ name:               postgrest-version:            9.0.0+version:            9.0.1 synopsis:           REST API for any Postgres database description:        Reads the schema of a PostgreSQL database and creates RESTful routes-                    for the tables and views, supporting all HTTP verbs that security+                    for tables, views, and functions, supporting all HTTP verbs that security                     permits. license:            MIT license-file:       LICENSE@@ -44,6 +44,7 @@                       PostgREST.Config.PgVersion                       PostgREST.Config.Proxy                       PostgREST.ContentType+                      PostgREST.Cors                       PostgREST.DbStructure                       PostgREST.DbStructure.Identifiers                       PostgREST.DbStructure.Proc@@ -51,6 +52,7 @@                       PostgREST.DbStructure.Table                       PostgREST.Error                       PostgREST.GucHeader+                      PostgREST.Logger                       PostgREST.Middleware                       PostgREST.OpenAPI                       PostgREST.Query.QueryBuilder@@ -69,7 +71,6 @@                     , HTTP                      >= 4000.3.7 && < 4000.4                     , Ranged-sets               >= 0.3 && < 0.5                     , aeson                     >= 1.4.7 && < 1.6-                    , ansi-wl-pprint            >= 0.6.7 && < 0.7                     , auto-update               >= 0.1.4 && < 0.2                     , base64-bytestring         >= 1 && < 1.3                     , bytestring                >= 0.10.8 && < 0.11@@ -77,11 +78,9 @@                     , cassava                   >= 0.4.5 && < 0.6                     , configurator-pg           >= 0.2 && < 0.3                     , containers                >= 0.5.7 && < 0.7-                    , contravariant             >= 1.4 && < 1.6                     , contravariant-extras      >= 0.3.3 && < 0.4                     , cookie                    >= 0.4.2 && < 0.5                     , either                    >= 4.4.1 && < 5.1-                    , fast-logger               >= 2.4.5                     , gitrev                    >= 1.2 && < 1.4                     , hasql                     >= 1.4 && < 1.5                     , hasql-dynamic-statements  == 0.3.1@@ -99,7 +98,7 @@                     , network-uri               >= 2.6.1 && < 2.8                     , optparse-applicative      >= 0.13 && < 0.17                     , parsec                    >= 3.1.11 && < 3.2-                    , protolude                 >= 0.3 && < 0.4+                    , protolude                 >= 0.3.1 && < 0.4                     , regex-tdfa                >= 1.2.2 && < 1.4                     , retry                     >= 0.7.4 && < 0.10                     , scientific                >= 0.3.4 && < 0.4@@ -110,10 +109,8 @@                     , vector                    >= 0.11 && < 0.13                     , wai                       >= 3.2.1 && < 3.3                     , wai-cors                  >= 0.2.5 && < 0.3-                    , wai-extra                 >= 3.0.19 && < 3.2-                    , wai-logger                >= 2.3.2-                    , wai-middleware-static     >= 0.8.1 && < 0.10-                    , warp                      >= 3.2.12 && < 3.4+                    , wai-extra                 >= 3.1.8 && < 3.2+                    , warp                      >= 3.3.19 && < 3.4                       -- -fno-spec-constr may help keep compile time memory use in check,                       --   see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304                       -- -optP-Wno-nonportable-include-path@@ -123,9 +120,9 @@                       -fno-spec-constr -optP-Wno-nonportable-include-path    if flag(dev)-    ghc-options: -O0+    ghc-options: -O0 -fwrite-ide-info     if flag(hpc)-      ghc-options: -hpcdir .hpc+      ghc-options: --enable-coverage -hpcdir .hpc   else     ghc-options: -O2 @@ -146,15 +143,17 @@   build-depends:      base                >= 4.9 && < 4.16                     , containers          >= 0.5.7 && < 0.7                     , postgrest-                    , protolude           >= 0.3 && < 0.4-  ghc-options:        -threaded -rtsopts "-with-rtsopts=-N -I2"+                    , protolude           >= 0.3.1 && < 0.4+  ghc-options:        -threaded -rtsopts "-with-rtsopts=-N -I0 -qg"                       -O2 -Wall -fwarn-identities                       -fno-spec-constr -optP-Wno-nonportable-include-path    if flag(dev)-    ghc-options: -O0+    ghc-options: -O0 -fwrite-ide-info+                 -- https://github.com/PostgREST/postgrest/issues/387+                 -with-rtsopts=-K1K     if flag(hpc)-      ghc-options: -hpcdir .hpc+      ghc-options: --enable-coverage -hpcdir .hpc   else     ghc-options: -O2 @@ -164,7 +163,7 @@   default-extensions: OverloadedStrings                       QuasiQuotes                       NoImplicitPrelude-  hs-source-dirs:     test+  hs-source-dirs:     test/spec   main-is:            Main.hs   other-modules:      Feature.AndOrParamsSpec                       Feature.AsymmetricJwtSpec@@ -211,10 +210,7 @@                     , base64-bytestring >= 1 && < 1.3                     , bytestring        >= 0.10.8 && < 0.11                     , case-insensitive  >= 1.2 && < 1.3-                    , cassava           >= 0.4.5 && < 0.6                     , containers        >= 0.5.7 && < 0.7-                    , contravariant     >= 1.4 && < 1.6-                    , hasql             >= 1.4 && < 1.5                     , hasql-pool        >= 0.5 && < 0.6                     , hasql-transaction >= 1.0.1 && < 1.1                     , heredoc           >= 0.2 && < 0.3@@ -227,72 +223,67 @@                     , monad-control     >= 1.0.1 && < 1.1                     , postgrest                     , process           >= 1.4.2 && < 1.7-                    , protolude         >= 0.3 && < 0.4+                    , protolude         >= 0.3.1 && < 0.4                     , regex-tdfa        >= 1.2.2 && < 1.4                     , text              >= 1.2.2 && < 1.3-                    , time              >= 1.6 && < 1.11                     , transformers-base >= 0.4.4 && < 0.5                     , wai               >= 3.2.1 && < 3.3                     , wai-extra         >= 3.0.19 && < 3.2   ghc-options:        -O0 -Wall -fwarn-identities                       -fno-spec-constr -optP-Wno-nonportable-include-path                       -fno-warn-missing-signatures+                      -fwrite-ide-info+                      -- https://github.com/PostgREST/postgrest/issues/387+                      -with-rtsopts=-K33K  test-suite querycost-  type:                exitcode-stdio-1.0-  default-language:    Haskell2010-  default-extensions:  OverloadedStrings-                       QuasiQuotes-                       NoImplicitPrelude-  hs-source-dirs:      test-  main-is:             QueryCost.hs-  other-modules:       SpecHelper-  build-depends:       base              >= 4.9 && < 4.16-                     , aeson             >= 1.4.7 && < 1.6-                     , aeson-qq          >= 0.8.1 && < 0.9-                     , async             >= 2.1.1 && < 2.3-                     , auto-update       >= 0.1.4 && < 0.2-                     , base64-bytestring >= 1 && < 1.3-                     , bytestring        >= 0.10.8 && < 0.11-                     , case-insensitive  >= 1.2 && < 1.3-                     , cassava           >= 0.4.5 && < 0.6-                     , containers        >= 0.5.7 && < 0.7-                     , contravariant     >= 1.4 && < 1.6-                     , hasql             >= 1.4 && < 1.5-                     , hasql-dynamic-statements  == 0.3.1-                     , hasql-pool        >= 0.5 && < 0.6-                     , hasql-transaction >= 1.0.1 && < 1.1-                     , heredoc           >= 0.2 && < 0.3-                     , hspec             >= 2.3 && < 2.9-                     , hspec-wai         >= 0.10 && < 0.12-                     , hspec-wai-json    >= 0.10 && < 0.12-                     , http-types        >= 0.12.3 && < 0.13-                     , lens              >= 4.14 && < 5.1-                     , lens-aeson        >= 1.0.1 && < 1.2-                     , monad-control     >= 1.0.1 && < 1.1-                     , postgrest-                     , process           >= 1.4.2 && < 1.7-                     , protolude         >= 0.3 && < 0.4-                     , regex-tdfa        >= 1.2.2 && < 1.4-                     , text              >= 1.2.2 && < 1.3-                     , time              >= 1.6 && < 1.11-                     , transformers-base >= 0.4.4 && < 0.5-                     , wai               >= 3.2.1 && < 3.3-                     , wai-extra         >= 3.0.19 && < 3.2-  ghc-options:         -O0 -Wall -fwarn-identities-                       -fno-spec-constr -optP-Wno-nonportable-include-path+  type:               exitcode-stdio-1.0+  default-language:   Haskell2010+  default-extensions: OverloadedStrings+                      QuasiQuotes+                      NoImplicitPrelude+  hs-source-dirs:     test/spec+  main-is:            QueryCost.hs+  other-modules:      SpecHelper+  build-depends:      base              >= 4.9 && < 4.16+                    , aeson             >= 1.4.7 && < 1.6+                    , base64-bytestring >= 1 && < 1.3+                    , bytestring        >= 0.10.8 && < 0.11+                    , case-insensitive  >= 1.2 && < 1.3+                    , containers        >= 0.5.7 && < 0.7+                    , contravariant     >= 1.4 && < 1.6+                    , hasql             >= 1.4 && < 1.5+                    , hasql-dynamic-statements  == 0.3.1+                    , hasql-pool        >= 0.5 && < 0.6+                    , hasql-transaction >= 1.0.1 && < 1.1+                    , heredoc           >= 0.2 && < 0.3+                    , hspec             >= 2.3 && < 2.9+                    , hspec-wai         >= 0.10 && < 0.12+                    , http-types        >= 0.12.3 && < 0.13+                    , lens              >= 4.14 && < 5.1+                    , lens-aeson        >= 1.0.1 && < 1.2+                    , postgrest+                    , process           >= 1.4.2 && < 1.7+                    , protolude         >= 0.3.1 && < 0.4+                    , regex-tdfa        >= 1.2.2 && < 1.4+                    , wai-extra         >= 3.0.19 && < 3.2+  ghc-options:        -O0 -Wall -fwarn-identities+                      -fno-spec-constr -optP-Wno-nonportable-include-path+                      -fwrite-ide-info+                      -- https://github.com/PostgREST/postgrest/issues/387+                      -with-rtsopts=-K1K  test-suite doctests-  type:                exitcode-stdio-1.0-  default-language:    Haskell2010-  default-extensions:  OverloadedStrings-                       NoImplicitPrelude-  hs-source-dirs:      test/doctests-  main-is:             Main.hs-  build-depends:       base              >= 4.9 && < 4.16-                     , doctest           >= 0.8-                     , postgrest-                     , pretty-simple-                     , protolude         >= 0.3 && < 0.4-  ghc-options:         -threaded -O0 -Wall -fwarn-identities-                       -fno-spec-constr -optP-Wno-nonportable-include-path+  type:               exitcode-stdio-1.0+  default-language:   Haskell2010+  default-extensions: OverloadedStrings+                      NoImplicitPrelude+  hs-source-dirs:     test/doc+  main-is:            Main.hs+  build-depends:      base              >= 4.9 && < 4.16+                    , doctest           >= 0.8+                    , postgrest+                    , pretty-simple+                    , protolude         >= 0.3.1 && < 0.4+  ghc-options:        -threaded -O0 -Wall -fwarn-identities+                      -fno-spec-constr -optP-Wno-nonportable-include-path
src/PostgREST/App.hs view
@@ -42,8 +42,10 @@  import qualified PostgREST.AppState                 as AppState import qualified PostgREST.Auth                     as Auth+import qualified PostgREST.Cors                     as Cors import qualified PostgREST.DbStructure              as DbStructure import qualified PostgREST.Error                    as Error+import qualified PostgREST.Logger                   as Logger import qualified PostgREST.Middleware               as Middleware import qualified PostgREST.OpenAPI                  as OpenAPI import qualified PostgREST.Query.QueryBuilder       as QueryBuilder@@ -137,8 +139,9 @@  -- | PostgREST application postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application-postgrest logLev appState connWorker =-  Middleware.pgrstMiddleware logLev $+postgrest logLevel appState connWorker =+  Logger.middleware logLevel .+  Cors.middleware $     \req respond -> do       time <- AppState.getTime appState       conf <- AppState.getConfig appState@@ -466,7 +469,7 @@       OAFollowPriv ->         OpenAPI.encode conf dbStructure            <$> SQL.statement tSchema (DbStructure.accessibleTables ctxPgVersion configDbPreparedStatements)-           <*> SQL.statement tSchema (DbStructure.accessibleProcs configDbPreparedStatements)+           <*> SQL.statement tSchema (DbStructure.accessibleProcs ctxPgVersion configDbPreparedStatements)            <*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)       OAIgnorePriv ->         OpenAPI.encode conf dbStructure
src/PostgREST/CLI.hs view
@@ -53,7 +53,6 @@ dumpSchema :: AppState -> IO LBS.ByteString dumpSchema appState = do   AppConfig{..} <- AppState.getConfig appState-  actualPgVersion <- AppState.getPgVersion appState   result <-     let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in     SQL.use (AppState.getPool appState) $@@ -61,7 +60,6 @@         queryDbStructure           (toList configDbSchemas)           configDbExtraSearchPath-          actualPgVersion           configDbPreparedStatements   SQL.release $ AppState.getPool appState   case result of
src/PostgREST/Config.hs view
@@ -131,7 +131,7 @@       ,("db-prepared-statements",        T.toLower . show . configDbPreparedStatements)       ,("db-root-spec",              q . maybe mempty dumpQi . configDbRootSpec)       ,("db-schemas",                q . T.intercalate "," . toList . configDbSchemas)-      ,("db-config",                 q . T.toLower . show . configDbConfig)+      ,("db-config",                     T.toLower . show . configDbConfig)       ,("db-tx-end",                 q . showTxEnd)       ,("db-uri",                    q . configDbUri)       ,("db-use-legacy-gucs",            T.toLower . show . configDbUseLegacyGucs)@@ -369,17 +369,17 @@      coerceInt :: (Read i, Integral i) => C.Value -> Maybe i     coerceInt (C.Number x) = rightToMaybe $ floatingOrInteger x-    coerceInt (C.String x) = readMaybe $ toS x+    coerceInt (C.String x) = readMaybe x     coerceInt _            = Nothing      coerceBool :: C.Value -> Maybe Bool     coerceBool (C.Bool b)   = Just b     coerceBool (C.String s) =       -- parse all kinds of text: True, true, TRUE, "true", ...-      case readMaybe . toS $ T.toTitle $ T.filter isAlpha $ toS s of+      case readMaybe $ T.toTitle $ T.filter isAlpha $ toS s of         Just b  -> Just b         -- numeric instead?-        Nothing -> (> 0) <$> (readMaybe $ toS s :: Maybe Integer)+        Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer)     coerceBool _            = Nothing      splitOnCommas :: C.Value -> [Text]
src/PostgREST/Config/Database.hs view
@@ -1,7 +1,8 @@ {-# LANGUAGE QuasiQuotes #-}  module PostgREST.Config.Database-  ( queryDbSettings+  ( pgVersionStatement+  , queryDbSettings   , queryPgVersion   ) where @@ -20,7 +21,10 @@ import Protolude  queryPgVersion :: Session PgVersion-queryPgVersion = statement mempty $ SQL.Statement sql HE.noParams versionRow False+queryPgVersion = statement mempty pgVersionStatement++pgVersionStatement :: SQL.Statement () PgVersion+pgVersionStatement = SQL.Statement sql HE.noParams versionRow False   where     sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"     versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text@@ -36,19 +40,26 @@ dbSettingsStatement = SQL.Statement sql HE.noParams decodeSettings False   where     sql = [q|-      with-      role_setting as (-        select setdatabase, unnest(setconfig) as setting from pg_catalog.pg_db_role_setting-        where setrole = current_user::regrole::oid-          and setdatabase in (0, (select oid from pg_catalog.pg_database where datname = current_catalog))+      WITH+      role_setting (database, setting) AS (+        SELECT setdatabase,+               unnest(setconfig)+          FROM pg_catalog.pg_db_role_setting+         WHERE setrole = CURRENT_USER::regrole::oid+           AND setdatabase IN (0, (SELECT oid FROM pg_catalog.pg_database WHERE datname = CURRENT_CATALOG))       ),-      kv_settings as (-        select setdatabase, split_part(setting, '=', 1) as k, split_part(setting, '=', 2) as value from role_setting-        where setting like 'pgrst.%'+      kv_settings (database, k, v) AS (+        SELECT database,+               substr(setting, 1, strpos(setting, '=') - 1),+               substr(setting, strpos(setting, '=') + 1)+          FROM role_setting+         WHERE setting LIKE 'pgrst.%'       )-      select distinct on (key) replace(k, 'pgrst.', '') as key, value-      from kv_settings-      order by key, setdatabase desc;+      SELECT DISTINCT ON (key)+             replace(k, 'pgrst.', '') AS key,+             v AS value+        FROM kv_settings+       ORDER BY key, database DESC;     |]     decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text 
+ src/PostgREST/Cors.hs view
@@ -0,0 +1,42 @@+{-|+Module      : PostgREST.Cors+Description : Wai Middleware to set cors policy.+-}+module PostgREST.Cors (middleware) where++import qualified Data.ByteString.Char8       as BS+import qualified Data.CaseInsensitive        as CI+import qualified Network.Wai                 as Wai+import qualified Network.Wai.Middleware.Cors as Wai++import Data.List (lookup)++import Protolude++middleware :: Wai.Middleware+middleware = Wai.cors corsPolicy++-- | CORS policy to be used in by Wai Cors middleware+corsPolicy :: Wai.Request -> Maybe Wai.CorsResourcePolicy+corsPolicy req = case lookup "origin" headers of+  Just origin ->+    Just Wai.CorsResourcePolicy+    { Wai.corsOrigins = Just ([origin], True)+    , Wai.corsMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]+    , Wai.corsRequestHeaders = "Authorization" : accHeaders+    , Wai.corsExposedHeaders = Just+      [ "Content-Encoding", "Content-Location", "Content-Range", "Content-Type"+      , "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"]+    , Wai.corsMaxAge = Just $ 60*60*24+    , Wai.corsVaryOrigin = False+    , Wai.corsRequireOrigin = False+    , Wai.corsIgnoreFailures = True+    }+  Nothing -> Nothing+  where+    headers = Wai.requestHeaders req+    accHeaders = case lookup "access-control-request-headers" headers of+      Just hdrs -> map (CI.mk . BS.strip) $ BS.split ',' hdrs+       -- Impossible case, Middleware.Cors will not evaluate this when+       -- the Access-Control-Request-Headers header is not set.+      Nothing   -> []
src/PostgREST/DbStructure.hs view
@@ -41,7 +41,9 @@ import Data.Text                     (split) import Text.InterpolatedString.Perl6 (q) -import PostgREST.Config.PgVersion         (PgVersion, pgVersion100)+import PostgREST.Config.Database          (pgVersionStatement)+import PostgREST.Config.PgVersion         (PgVersion, pgVersion100,+                                           pgVersion110) import PostgREST.DbStructure.Identifiers  (QualifiedIdentifier (..),                                            Schema, TableName) import PostgREST.DbStructure.Proc         (PgType (..),@@ -83,15 +85,16 @@ -- | A SQL query that can be executed independently type SqlQuery = ByteString -queryDbStructure :: [Schema] -> [Schema] -> PgVersion -> Bool -> SQL.Transaction DbStructure-queryDbStructure schemas extraSearchPath pgVer prepared = do+queryDbStructure :: [Schema] -> [Schema] -> Bool -> SQL.Transaction DbStructure+queryDbStructure schemas extraSearchPath prepared = do   SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object+  pgVer   <- SQL.statement mempty pgVersionStatement   tabs    <- SQL.statement mempty $ allTables pgVer prepared   cols    <- SQL.statement schemas $ allColumns tabs prepared   srcCols <- SQL.statement (schemas, extraSearchPath) $ pfkSourceColumns cols prepared   m2oRels <- SQL.statement mempty $ allM2ORels tabs cols prepared   keys    <- SQL.statement mempty $ allPrimaryKeys tabs prepared-  procs   <- SQL.statement schemas $ allProcs prepared+  procs   <- SQL.statement schemas $ allProcs pgVer prepared    let rels = addO2MRels . addM2MRels $ addViewM2ORels srcCols m2oRels       keys' = addViewPrimaryKeys srcCols keys@@ -224,18 +227,18 @@                       | v == 's' = Stable                       | otherwise = Volatile -- only 'v' can happen here -allProcs :: Bool -> SQL.Statement [Schema] ProcsMap-allProcs = SQL.Statement sql (arrayParam HE.text) decodeProcs+allProcs :: PgVersion -> Bool -> SQL.Statement [Schema] ProcsMap+allProcs pgVer = SQL.Statement sql (arrayParam HE.text) decodeProcs   where-    sql = procsSqlQuery <> " WHERE pn.nspname = ANY($1)"+    sql = procsSqlQuery pgVer <> " AND pn.nspname = ANY($1)" -accessibleProcs :: Bool -> SQL.Statement Schema ProcsMap-accessibleProcs = SQL.Statement sql (param HE.text) decodeProcs+accessibleProcs :: PgVersion -> Bool -> SQL.Statement Schema ProcsMap+accessibleProcs pgVer = SQL.Statement sql (param HE.text) decodeProcs   where-    sql = procsSqlQuery <> " WHERE pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')"+    sql = procsSqlQuery pgVer <> " AND pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')" -procsSqlQuery :: SqlQuery-procsSqlQuery = [q|+procsSqlQuery :: PgVersion -> SqlQuery+procsSqlQuery pgVer = [q|  -- Recursively get the base types of domains   WITH   base_types AS (@@ -297,7 +300,8 @@   JOIN pg_namespace tn ON tn.oid = t.typnamespace   LEFT JOIN pg_class comp ON comp.oid = t.typrelid   LEFT JOIN pg_catalog.pg_description as d ON d.objoid = p.oid-|]+  WHERE t.oid <> 'pg_catalog.trigger'::regtype+|] <> (if pgVer >= pgVersion110 then "AND prokind = 'f'" else "AND NOT (proisagg OR proiswindow)")  schemaDescription :: Bool -> SQL.Statement Schema (Maybe Text) schemaDescription =@@ -792,7 +796,6 @@             replace(             replace(             replace(-            replace(             regexp_replace(             replace(             replace(@@ -804,6 +807,7 @@             replace(             replace(             replace(+            replace(               view_definition::text,             -- This conversion to json is heavily optimized for performance.             -- The general idea is to use as few regexp_replace() calls as possible.@@ -814,9 +818,15 @@             -- -----------------------------------------------             -- pattern           | replacement         | flags             -- -----------------------------------------------+            -- `<>` in pg_node_tree is the same as `null` in JSON, but due to very poor performance of json_typeof+            -- we need to make this an empty array here to prevent json_array_elements from throwing an error+            -- when the targetList is null.+            -- We'll need to put it first, to make the node protection below work for node lists that start with+            -- null: `(<> ...`, too. This is the case for coldefexprs, when the first column does not have a default value.+               '<>'              , '()'             -- `,` is not part of the pg_node_tree format, but used in the regex.             -- This removes all `,` that might be part of column names.-               ','               , ''+            ), ','               , ''             -- The same applies for `{` and `}`, although those are used a lot in pg_node_tree.             -- We remove the escaped ones, which might be part of column names again.             ), E'\\{'            , ''@@ -851,10 +861,6 @@             ), ')'               , ']'             -- pg_node_tree has ` ` between list items, but JSON uses `,`             ), ' '             , ','-            -- `<>` in pg_node_tree is the same as `null` in JSON, but due to very poor performance of json_typeof-            -- we need to make this an empty array here to prevent json_array_elements from throwing an error-            -- when the targetList is null.-            ), '<>'              , '[]'           )::json as view_definition         from views       ),
src/PostgREST/Error.hs view
@@ -126,23 +126,22 @@ compressedRel :: Relationship -> JSON.Value compressedRel Relationship{..} =   let-    fmtTbl Table{..} = tableSchema <> "." <> tableName-    fmtEls els = "[" <> T.intercalate ", " els <> "]"+    fmtEls els = "(" <> T.intercalate ", " els <> ")"   in   JSON.object $     ("embedding" .= (tableName relTable <> " with " <> tableName relForeignTable :: Text))     : case relCardinality of         M2M Junction{..} -> [             "cardinality" .= ("many-to-many" :: Text)-          , "relationship" .= (fmtTbl junTable <> fmtEls [junConstraint1] <> fmtEls [junConstraint2])+          , "relationship" .= (tableName junTable <> " using " <> junConstraint1 <> fmtEls (colName <$> junColumns1) <> " and " <> junConstraint2 <> fmtEls (colName <$> junColumns2))           ]         M2O cons -> [             "cardinality" .= ("many-to-one" :: Text)-          , "relationship" .= (cons <> fmtEls (colName <$> relColumns) <> fmtEls (colName <$> relForeignColumns))+          , "relationship" .= (cons <> " using " <> tableName relTable <> fmtEls (colName <$> relColumns) <> " and " <> tableName relForeignTable <> fmtEls (colName <$> relForeignColumns))           ]         O2M cons -> [             "cardinality" .= ("one-to-many" :: Text)-          , "relationship" .= (cons <> fmtEls (colName <$> relColumns) <> fmtEls (colName <$> relForeignColumns))+          , "relationship" .= (cons <> " using " <> tableName relTable <> fmtEls (colName <$> relColumns) <> " and " <> tableName relForeignTable <> fmtEls (colName <$> relForeignColumns))           ]  relHint :: [Relationship] -> Text
+ src/PostgREST/Logger.hs view
@@ -0,0 +1,28 @@+{-|+Module      : PostgREST.Logger+Description : Wai Middleware to log requests to stdout.+-}+module PostgREST.Logger (middleware) where++import qualified Network.Wai                          as Wai+import qualified Network.Wai.Middleware.RequestLogger as Wai++import Network.HTTP.Types.Status (status400, status500)+import System.IO.Unsafe          (unsafePerformIO)++import PostgREST.Config (LogLevel (..))++import Protolude++middleware :: LogLevel -> Wai.Middleware+middleware logLevel = case logLevel of+  LogInfo  -> requestLogger (const True)+  LogWarn  -> requestLogger (>= status400)+  LogError -> requestLogger (>= status500)+  LogCrit  -> requestLogger (const False)+  where+    requestLogger filterStatus = unsafePerformIO $ Wai.mkRequestLogger Wai.defaultRequestLoggerSettings+      { Wai.outputFormat = Wai.ApacheWithSettings $+          Wai.defaultApacheSettings+            & Wai.setApacheRequestFilter (\_ res -> filterStatus $ Wai.responseStatus res)+      }
src/PostgREST/Middleware.hs view
@@ -2,47 +2,29 @@ Module      : PostgREST.Middleware Description : Sets CORS policy. Also the PostgreSQL GUCs, role, search_path and pre-request function. -}+{-# LANGUAGE BlockArguments  #-} {-# LANGUAGE RecordWildCards #-} module PostgREST.Middleware   ( runPgLocals-  , pgrstFormat-  , pgrstMiddleware-  , defaultCorsPolicy-  , corsPolicy   , optionalRollback   ) where -import qualified Data.Aeson                           as JSON-import qualified Data.ByteString.Char8                as BS-import qualified Data.ByteString.Lazy.Char8           as LBS-import qualified Data.CaseInsensitive                 as CI-import qualified Data.HashMap.Strict                  as M-import qualified Data.Text                            as T-import qualified Data.Text.Encoding                   as T-import qualified Hasql.Decoders                       as HD-import qualified Hasql.DynamicStatements.Snippet      as SQL hiding-                                                             (sql)-import qualified Hasql.DynamicStatements.Statement    as SQL-import qualified Hasql.Transaction                    as SQL-import qualified Network.Wai                          as Wai-import qualified Network.Wai.Logger                   as Wai-import qualified Network.Wai.Middleware.Cors          as Wai-import qualified Network.Wai.Middleware.Gzip          as Wai-import qualified Network.Wai.Middleware.RequestLogger as Wai-import qualified Network.Wai.Middleware.Static        as Wai+import qualified Data.Aeson                        as JSON+import qualified Data.ByteString.Lazy.Char8        as LBS+import qualified Data.HashMap.Strict               as M+import qualified Data.Text                         as T+import qualified Data.Text.Encoding                as T+import qualified Hasql.Decoders                    as HD+import qualified Hasql.DynamicStatements.Snippet   as SQL hiding (sql)+import qualified Hasql.DynamicStatements.Statement as SQL+import qualified Hasql.Transaction                 as SQL+import qualified Network.Wai                       as Wai  import Control.Arrow ((***)) -import Data.Function             (id)-import Data.List                 (lookup)-import Data.Scientific           (FPFormat (..), formatScientific,-                                  isInteger)-import Network.HTTP.Types.Status (Status, status400, status500,-                                  statusCode)-import System.IO.Unsafe          (unsafePerformIO)-import System.Log.FastLogger     (toLogStr)+import Data.Scientific (FPFormat (..), formatScientific, isInteger) -import PostgREST.Config             (AppConfig (..), LogLevel (..))+import PostgREST.Config             (AppConfig (..)) import PostgREST.Config.PgVersion   (PgVersion (..), pgVersion140) import PostgREST.Error              (Error, errorResponseFor) import PostgREST.GucHeader          (addHeadersIfNotIncluded)@@ -90,76 +72,12 @@       _                             -> mempty     usesLegacyGucs = configDbUseLegacyGucs conf && actualPgVersion < pgVersion140 --- | Log in apache format. Only requests that have a status greater than minStatus are logged.--- | There's no way to filter logs in the apache format on wai-extra: https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html#t:OutputFormat.--- | So here we copy wai-logger apacheLogStr function: https://github.com/kazu-yamamoto/logger/blob/a4f51b909a099c51af7a3f75cf16e19a06f9e257/wai-logger/Network/Wai/Logger/Apache.hs#L45--- | TODO: Add the ability to filter apache logs on wai-extra and remove this function.-pgrstFormat :: Status -> Wai.OutputFormatter-pgrstFormat minStatus date req status responseSize =-  if status < minStatus-    then mempty-  else toLogStr (getSourceFromSocket req)-    <> " - - ["-    <> toLogStr date-    <> "] \""-    <> toLogStr (Wai.requestMethod req)-    <> " "-    <> toLogStr (Wai.rawPathInfo req <> Wai.rawQueryString req)-    <> " "-    <> toLogStr (show (Wai.httpVersion req)::Text)-    <> "\" "-    <> toLogStr (show (statusCode status)::Text)-    <> " "-    <> toLogStr (maybe "-" show responseSize::Text)-    <> " \""-    <> toLogStr (fromMaybe mempty $ Wai.requestHeaderReferer req)-    <> "\" \""-    <> toLogStr (fromMaybe mempty $ Wai.requestHeaderUserAgent req)-    <> "\"\n"-  where-    getSourceFromSocket = BS.pack . Wai.showSockAddr . Wai.remoteHost--pgrstMiddleware :: LogLevel -> Wai.Application -> Wai.Application-pgrstMiddleware logLevel =-    logger-  . Wai.cors corsPolicy-  . Wai.staticPolicy (Wai.only [("favicon.ico", "static/favicon.ico")])-  where-    logger = case logLevel of-      LogCrit  -> id-      LogError -> unsafePerformIO $ Wai.mkRequestLogger Wai.def { Wai.outputFormat = Wai.CustomOutputFormat $ pgrstFormat status500}-      LogWarn  -> unsafePerformIO $ Wai.mkRequestLogger Wai.def { Wai.outputFormat = Wai.CustomOutputFormat $ pgrstFormat status400}-      LogInfo  -> Wai.logStdout--defaultCorsPolicy :: Wai.CorsResourcePolicy-defaultCorsPolicy =  Wai.CorsResourcePolicy Nothing-  ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"] ["Authorization"] Nothing-  (Just $ 60*60*24) False False True---- | CORS policy to be used in by Wai Cors middleware-corsPolicy :: Wai.Request -> Maybe Wai.CorsResourcePolicy-corsPolicy req = case lookup "origin" headers of-  Just origin -> Just defaultCorsPolicy {-      Wai.corsOrigins = Just ([origin], True)-    , Wai.corsRequestHeaders = "Authentication" : accHeaders-    , Wai.corsExposedHeaders = Just [-        "Content-Encoding", "Content-Location", "Content-Range", "Content-Type"-      , "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"-      ]-    }-  Nothing -> Nothing-  where-    headers = Wai.requestHeaders req-    accHeaders = case lookup "access-control-request-headers" headers of-      Just hdrs -> map (CI.mk . BS.strip) $ BS.split ',' hdrs-      Nothing   -> []--unquoted :: JSON.Value -> Text-unquoted (JSON.String t) = t-unquoted (JSON.Number n) =-  toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n-unquoted (JSON.Bool b) = show b-unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v+    unquoted :: JSON.Value -> Text+    unquoted (JSON.String t) = t+    unquoted (JSON.Number n) =+      toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n+    unquoted (JSON.Bool b) = show b+    unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v  -- | Set a transaction to eventually roll back if requested and set respective -- headers on the response.@@ -170,8 +88,9 @@   -> ExceptT Error SQL.Transaction Wai.Response optionalRollback AppConfig{..} ApiRequest{..} transaction = do   resp <- catchError transaction $ return . errorResponseFor-  when (shouldRollback || (configDbTxRollbackAll && not shouldCommit))-    (lift SQL.condemn)+  when (shouldRollback || (configDbTxRollbackAll && not shouldCommit)) $ lift do+    SQL.sql "SET CONSTRAINTS ALL IMMEDIATE"+    SQL.condemn   return $ Wai.mapResponseHeaders preferenceApplied resp   where     shouldCommit =
src/PostgREST/OpenAPI.hs view
@@ -55,24 +55,26 @@ makeMimeList :: [ContentType] -> MimeList makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs -toSwaggerType :: Text -> SwaggerType t-toSwaggerType "character varying" = SwaggerString-toSwaggerType "character"         = SwaggerString-toSwaggerType "text"              = SwaggerString-toSwaggerType "boolean"           = SwaggerBoolean-toSwaggerType "smallint"          = SwaggerInteger-toSwaggerType "integer"           = SwaggerInteger-toSwaggerType "bigint"            = SwaggerInteger-toSwaggerType "numeric"           = SwaggerNumber-toSwaggerType "real"              = SwaggerNumber-toSwaggerType "double precision"  = SwaggerNumber-toSwaggerType "ARRAY"             = SwaggerArray-toSwaggerType _                   = SwaggerString+toSwaggerType :: Text -> Maybe (SwaggerType t)+toSwaggerType "character varying" = Just SwaggerString+toSwaggerType "character"         = Just SwaggerString+toSwaggerType "text"              = Just SwaggerString+toSwaggerType "boolean"           = Just SwaggerBoolean+toSwaggerType "smallint"          = Just SwaggerInteger+toSwaggerType "integer"           = Just SwaggerInteger+toSwaggerType "bigint"            = Just SwaggerInteger+toSwaggerType "numeric"           = Just SwaggerNumber+toSwaggerType "real"              = Just SwaggerNumber+toSwaggerType "double precision"  = Just SwaggerNumber+toSwaggerType "ARRAY"             = Just SwaggerArray+toSwaggerType "json"              = Nothing+toSwaggerType "jsonb"             = Nothing+toSwaggerType _                   = Just SwaggerString  parseDefault :: Text -> Text -> Text parseDefault colType colDefault =   case toSwaggerType colType of-    SwaggerString -> wrapInQuotations $ case T.stripSuffix ("::" <> colType) colDefault of+    Just SwaggerString -> wrapInQuotations $ case T.stripSuffix ("::" <> colType) colDefault of       Just def -> T.dropAround (=='\'')  def       Nothing  -> colDefault     _ -> colDefault@@ -124,7 +126,7 @@         & enum_ .~ e         & format ?~ colType c         & maxLength .~ (fromIntegral <$> colMaxLen c)-        & type_ ?~ toSwaggerType (colType c)+        & type_ .~ toSwaggerType (colType c)  makeProcSchema :: ProcDescription -> Schema makeProcSchema pd =@@ -138,7 +140,7 @@ makeProcProperty (ProcParam n t _ _) = (n, Inline s)   where     s = (mempty :: Schema)-          & type_ ?~ toSwaggerType t+          & type_ .~ toSwaggerType t           & format ?~ t  makePreferParam :: [Text] -> Param
src/PostgREST/Query/SqlFragment.hs view
@@ -172,10 +172,10 @@   | returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar), '[]')::character varying"   | otherwise     = "coalesce(json_agg(_postgrest_t), '[]')::character varying" -asJsonSingleF :: Bool -> SqlFragment --TODO! unsafe when the query actually returns multiple rows, used only on inserting and returning single element+asJsonSingleF :: Bool -> SqlFragment asJsonSingleF returnsScalar-  | returnsScalar = "coalesce(string_agg(to_json(_postgrest_t.pgrst_scalar)::text, ','), 'null')::character varying"-  | otherwise     = "coalesce(string_agg(to_json(_postgrest_t)::text, ','), '')::character varying"+  | returnsScalar = "coalesce((json_agg(_postgrest_t.pgrst_scalar)->0)::text, 'null')"+  | otherwise     = "coalesce((json_agg(_postgrest_t)->0)::text, 'null')"  asBinaryF :: FieldName -> SqlFragment asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"
src/PostgREST/Request/ApiRequest.hs view
@@ -364,9 +364,9 @@   headerRange = rangeRequested hdrs   replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) ++ [x]   limitParams :: M.HashMap Text NonnegRange-  limitParams  = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe . toS =<< v) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]+  limitParams  = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe =<< v) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]   offsetParams :: M.HashMap Text NonnegRange-  offsetParams = M.fromList [(toS (replaceLast "limit" k), maybe allRange rangeGeq (readMaybe . toS =<< v)) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]+  offsetParams = M.fromList [(toS (replaceLast "limit" k), maybe allRange rangeGeq (readMaybe =<< v)) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]    urlRange = M.unionWith f limitParams offsetParams     where@@ -505,7 +505,7 @@         then length params == 1 && (ppType <$> headMay params) `elem` [Just "json", Just "jsonb"]       -- If the function has no parameters, the arguments keys must be empty as well       else if null params-        then null argumentsKeys && contentType `notElem` [CTTextPlain, CTOctetStream]+        then null argumentsKeys && not (isInvPost && contentType `elem` [CTTextPlain, CTOctetStream])       -- A function has optional and required parameters. Optional parameters have a default value and       -- don't require arguments for the function to be executed, required parameters must have an argument present.       else case L.partition ppReq params of
src/PostgREST/Request/DbRequestBuilder.hs view
@@ -56,7 +56,7 @@  import qualified PostgREST.DbStructure.Relationship as Relationship -import Protolude hiding (from)+import Protolude hiding (from, isInfixOf)  -- | Builds the ReadRequest tree on a number of stages. -- | Adds filters, order, limits on its respective nodes.
src/PostgREST/Request/Parsers.hs view
@@ -199,10 +199,10 @@       <|> pFts       <?> "operator (eq, gt, ...)" -    pTriVal = try (string "null"    $> TriNull)-          <|> try (string "unknown" $> TriUnknown)-          <|> try (string "true"    $> TriTrue)-          <|> try (string "false"   $> TriFalse)+    pTriVal = try (ciString "null"    $> TriNull)+          <|> try (ciString "unknown" $> TriUnknown)+          <|> try (ciString "true"    $> TriTrue)+          <|> try (ciString "false"   $> TriFalse)           <?> "null or trilean value (unknown, true, false)"      pFts = do@@ -212,6 +212,12 @@      ops = M.filterWithKey (const . flip notElem ("in":"is":ftsOps)) operators     ftsOps = M.keys ftsOperators++    -- case insensitive char and string+    ciChar :: Char -> GenParser Char state Char+    ciChar c = char c <|> char (toUpper c)+    ciString :: [Char] -> GenParser Char state [Char]+    ciString = traverse ciChar  pSingleVal :: Parser SingleVal pSingleVal = toS <$> many anyChar
src/PostgREST/Version.hs view
@@ -30,10 +30,12 @@   -- | Version number used in docs.+-- Pre-release versions link to the latest docs -- Uses only the two first components of the version. Example: 'v1.1' docsVersion :: Text-docsVersion =-  "v" <> (T.intercalate "." . map show . take 2 $ versionBranch version)+docsVersion+  | isPreRelease = "latest"+  | otherwise    =  "v" <> (T.intercalate "." . map show . take 2 $ versionBranch version)   -- | Versions with four components (e.g., '1.1.1.1') are treated as pre-releases.
src/PostgREST/Workers.hs view
@@ -152,11 +152,10 @@ loadSchemaCache :: AppState -> IO SCacheStatus loadSchemaCache appState = do   AppConfig{..} <- AppState.getConfig appState-  actualPgVersion <- AppState.getPgVersion appState   result <-     let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in     SQL.use (AppState.getPool appState) . transaction SQL.ReadCommitted SQL.Read $-      queryDbStructure (toList configDbSchemas) configDbExtraSearchPath actualPgVersion configDbPreparedStatements+      queryDbStructure (toList configDbSchemas) configDbExtraSearchPath configDbPreparedStatements   case result of     Left e -> do       let
− test/Feature/AndOrParamsSpec.hs
@@ -1,276 +0,0 @@-module Feature.AndOrParamsSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import PostgREST.Config.PgVersion (PgVersion, pgVersion112)--import Protolude  hiding (get)-import SpecHelper--spec :: PgVersion -> SpecWith ((), Application)-spec actualPgVersion =-  describe "and/or params used for complex boolean logic" $ do-    context "used with GET" $ do-      context "or param" $ do-        it "can do simple logic" $-          get "/entities?or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can negate simple logic" $-          get "/entities?not.or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`-            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can be combined with traditional filters" $-          get "/entities?or=(id.eq.1,id.eq.2)&name=eq.entity 1&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }--      context "embedded levels" $ do-        it "can do logic on the second level" $-          get "/entities?child_entities.or=(id.eq.1,name.eq.child entity 2)&select=id,child_entities(id)" `shouldRespondWith`-            [json|[-              {"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []},-              {"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}-            ]|] { matchHeaders = [matchContentTypeJson] }--        it "can do logic on the third level" $-          get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))"-            `shouldRespondWith`-              [json|[-                {"id": 1, "child_entities": [-                  { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]},-                  { "id": 2, "grandchild_entities": []},-                  { "id": 4, "grandchild_entities": []},-                  { "id": 5, "grandchild_entities": []}-                ]},-                {"id": 2, "child_entities": [-                  { "id": 3, "grandchild_entities": []},-                  { "id": 6, "grandchild_entities": []}-                ]},-                {"id": 3, "child_entities": []},-                {"id": 4, "child_entities": []}-              ]|]--      context "and/or params combined" $ do-        it "can be nested inside the same expression" $-          get "/entities?or=(and(name.eq.entity 2,id.eq.2),and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can be negated while nested" $-          get "/entities?or=(not.and(name.eq.entity 2,id.eq.2),not.and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can be combined unnested" $-          get "/entities?and=(id.eq.1,name.eq.entity 1)&or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }--      context "operators inside and/or" $ do-        it "can handle eq and neq" $-          get "/entities?and=(id.eq.1,id.neq.2))&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can handle lt and gt" $-          get "/entities?or=(id.lt.2,id.gt.3)&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can handle lte and gte" $-          get "/entities?or=(id.lte.2,id.gte.3)&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can handle like and ilike" $-          get "/entities?or=(name.like.*1,name.ilike.*ENTITY 2)&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can handle in" $-          get "/entities?or=(id.in.(1,2),id.in.(3,4))&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can handle is" $-          get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith`-            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-        it "can handle fts" $ do-          get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/tsearch?or=(text_search_vector.plfts(german).Art%20Spass, text_search_vector.plfts(french).amusant%20impossible, text_search_vector.fts(english).impossible)" `shouldRespondWith`-            [json|[-              {"text_search_vector": "'fun':5 'imposs':9 'kind':3" },-              {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },-              {"text_search_vector": "'art':4 'spass':5 'unmog':7"}-            ]|] { matchHeaders = [matchContentTypeJson] }--        when (actualPgVersion >= pgVersion112) $-          it "can handle wfts (websearch_to_tsquery)" $-            get "/tsearch?or=(text_search_vector.plfts(german).Art,text_search_vector.plfts(french).amusant,text_search_vector.not.wfts(english).impossible)"-            `shouldRespondWith`-              [json|[-                     {"text_search_vector": "'also':2 'fun':3 'possibl':8" },-                     {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" },-                     {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },-                     {"text_search_vector": "'art':4 'spass':5 'unmog':7" }-              ]|]-              { matchHeaders = [matchContentTypeJson] }--        it "can handle cs and cd" $-          get "/entities?or=(arr.cs.{1,2,3},arr.cd.{1})&select=id" `shouldRespondWith`-            [json|[{ "id": 1 },{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }--        it "can handle range operators" $ do-          get "/ranges?range=eq.[1,3]&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=neq.[1,3]&select=id" `shouldRespondWith`-            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=lt.[1,10]&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=gt.[8,11]&select=id" `shouldRespondWith`-            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=lte.[1,3]&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=gte.[2,3]&select=id" `shouldRespondWith`-            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=cs.[1,2]&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=cd.[1,6]&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=ov.[0,4]&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=sl.[9,10]&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=sr.[3,4]&select=id" `shouldRespondWith`-            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=nxr.[4,7]&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=nxl.[4,7]&select=id" `shouldRespondWith`-            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/ranges?range=adj.(3,10]&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }--        it "can handle array operators" $ do-          get "/entities?arr=eq.{1,2,3}&select=id" `shouldRespondWith`-            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=neq.{1,2}&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=lt.{2,3}&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=lt.{2,0}&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=gt.{1,1}&select=id" `shouldRespondWith`-            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=gt.{3}&select=id" `shouldRespondWith`-            [json|[]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=lte.{2,1}&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=lte.{1,2,3}&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=lte.{1,2}&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=cs.{1,2}&select=id" `shouldRespondWith`-            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=cd.{1,2,6}&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=ov.{3}&select=id" `shouldRespondWith`-            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?arr=ov.{2,3}&select=id" `shouldRespondWith`-            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }--        context "operators with not" $ do-          it "eq, cs, like can be negated" $-            get "/entities?and=(arr.not.cs.{1,2,3},and(id.not.eq.2,name.not.like.*3))&select=id" `shouldRespondWith`-              [json|[{ "id": 1}]|] { matchHeaders = [matchContentTypeJson] }-          it "in, is, fts can be negated" $-            get "/entities?and=(id.not.in.(1,3),and(name.not.is.null,text_search_vector.not.fts.foo))&select=id" `shouldRespondWith`-              [json|[{ "id": 2}]|] { matchHeaders = [matchContentTypeJson] }-          it "lt, gte, cd can be negated" $-            get "/entities?and=(arr.not.cd.{1},or(id.not.lt.1,id.not.gte.3))&select=id" `shouldRespondWith`-              [json|[{"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }-          it "gt, lte, ilike can be negated" $-            get "/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id" `shouldRespondWith`-              [json|[{"id": 1}, {"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }--      context "and/or params with quotes" $ do-        it "eq can have quotes" $-          get "/grandchild_entities?or=(name.eq.\"(grandchild,entity,4)\",name.eq.\"(grandchild,entity,5)\")&select=id" `shouldRespondWith`-            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }-        it "like and ilike can have quotes" $-          get "/grandchild_entities?or=(name.like.\"*ity,4*\",name.ilike.\"*ITY,5)\")&select=id" `shouldRespondWith`-            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }-        it "in can have quotes" $-          get "/grandchild_entities?or=(id.in.(\"1\",\"2\"),id.in.(\"3\",\"4\"))&select=id" `shouldRespondWith`-            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }--      it "allows whitespace" $-        get "/entities?and=( and ( id.in.( 1, 2, 3 ) , id.eq.3 ) , or ( id.eq.2 , id.eq.3 ) )&select=id" `shouldRespondWith`-          [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }--      context "multiple and/or conditions" $ do-        it "cannot have zero conditions" $-          get "/entities?or=()" `shouldRespondWith`-            [json|{-              "details": "unexpected \")\" expecting field name (* or [a..z0..9_]), negation operator (not) or logic operator (and, or)",-              "message": "\"failed to parse logic tree (())\" (line 1, column 4)"-            }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }-        it "can have a single condition" $ do-          get "/entities?or=(id.eq.1)&select=id" `shouldRespondWith`-            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }-          get "/entities?and=(id.eq.1)&select=id" `shouldRespondWith`-            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }-        it "can have three conditions" $ do-          get "/grandchild_entities?or=(id.eq.1, id.eq.2, id.eq.3)&select=id" `shouldRespondWith`-            [json|[{"id":1}, {"id":2}, {"id":3}]|] { matchHeaders = [matchContentTypeJson] }-          get "/grandchild_entities?and=(id.in.(1,2), id.in.(3,1), id.in.(1,4))&select=id" `shouldRespondWith`-            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }-        it "can have four conditions combining and/or" $ do-          get "/grandchild_entities?or=( id.eq.1, id.eq.2, and(id.in.(1,3), id.in.(2,3)), id.eq.4 )&select=id" `shouldRespondWith`-            [json|[{"id":1}, {"id":2}, {"id":3}, {"id":4}]|] { matchHeaders = [matchContentTypeJson] }-          get "/grandchild_entities?and=( id.eq.1, not.or(id.eq.2, id.eq.3), id.in.(1,4), or(id.eq.1, id.eq.4) )&select=id" `shouldRespondWith`-            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }--    context "used with POST" $-      it "includes related data with filters" $-        request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id"-            [("Prefer", "return=representation")]-            [json|[-              {"id":7,"name":"entity 4","parent_id":1},-              {"id":8,"name":"entity 5","parent_id":2},-              {"id":9,"name":"entity 6","parent_id":3}-            ]|]-          `shouldRespondWith`-            [json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|]-            { matchStatus = 201 }--    context "used with PATCH" $-      it "succeeds when using and/or params" $-        request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"-          [("Prefer", "return=representation")]-          [json|{ name : "updated grandchild entity"}|] `shouldRespondWith`-          [json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]-          { matchHeaders = [matchContentTypeJson] }--    context "used with DELETE" $-      it "succeeds when using and/or params" $-        request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"-            [("Prefer", "return=representation")]-            ""-          `shouldRespondWith`-            [json|[{ "id": 1, "name" : "grandchild entity 1" },{ "id": 2, "name" : "grandchild entity 2" }]|]--    it "can query columns that begin with and/or reserved words" $-      get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200--    it "fails when using IN without () and provides meaningful error message" $-      get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`-        [json|{-          "details": "unexpected \"1\" expecting \"(\"",-          "message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)"-        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }--    it "fails on malformed query params and provides meaningful error message" $ do-      get "/entities?or=)(" `shouldRespondWith`-        [json|{-          "details": "unexpected \")\" expecting \"(\"",-          "message": "\"failed to parse logic tree ()()\" (line 1, column 3)"-        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }-      get "/entities?and=(ord(id.eq.1,id.eq.1),id.eq.2)" `shouldRespondWith`-        [json|{-          "details": "unexpected \"d\" expecting \"(\"",-          "message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)"-        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }-      get "/entities?or=(id.eq.1,not.xor(id.eq.2,id.eq.3))" `shouldRespondWith`-        [json|{-          "details": "unexpected \"x\" expecting logic operator (and, or)",-          "message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)"-        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
− test/Feature/AsymmetricJwtSpec.hs
@@ -1,21 +0,0 @@-module Feature.AsymmetricJwtSpec where---- {{{ Imports-import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai--import Protolude-import SpecHelper--- }}}--spec :: SpecWith ((), Application)-spec = describe "server started with asymmetric JWK" $--  -- this test will stop working 9999999999s after the UNIX EPOCH-  it "succeeds with jwt token signed with an asymmetric key" $ do-    let auth = authHeaderJWT "eyJhbGciOiJSUzI1NiJ9.eyJyb2xlIjogInBvc3RncmVzdF90ZXN0X2F1dGhvciJ9Cg.CBOYWDvqgAR0YYnZnyDGTQi6AJLc2Pds6_eV3YuBG6I36mj_h05eLhkEKNEDA5ZteMzCiY83P60rC_xtxVd7B6vo3BeF5uoanPS3rrbuHzKPwzsrgrD_CqvEuJ4n7Q9epkQiLsNkcexneENZDRqFjbwZx3DrXiCWwlK3Ytr5NAIGxmy0od-0xNpb2U1nXQyO_Q3mumWFViRt4tmFn_3goDHNKG3Ha_AzImfUNvHnWL78kAc4rbn15vLtWXD8PwtSnZaB4lY4V6RfsaW937srQsmRetvytM1i_bHBnjkjQLAqGbXPyItjtlXPs0uGNBadE8-wgkLtfmSCC4v2DjUthw"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 200
− test/Feature/AudienceJwtSecretSpec.hs
@@ -1,47 +0,0 @@-module Feature.AudienceJwtSecretSpec where---- {{{ Imports-import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai--import Protolude  hiding (get)-import SpecHelper--- }}}--spec :: SpecWith ((), Application)-spec = describe "test handling of aud claims in JWT" $ do--  -- this test will stop working 9999999999s after the UNIX EPOCH-  it "succeeds with jwt token containing with an audience claim" $ do-    {-  This is the decoded contents of authHeaderJWT--        {-          "exp": 9999999999,-          "role": "postgrest_test_author",-          "id": "jdoe",-          "aud": "youraudience"-        }--    -}-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UiLCJhdWQiOiJ5b3VyYXVkaWVuY2UifQ.fJ4tLKSmolWGWehWN20qiU9dMO-WY0RI2VvacL7-ZGo"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 200--  it "succeeds with jwt token that does not contain an audience claim" $ do-    {- This is the decoded contents of authHeaderJWT--        {-          "exp": 9999999999,-          "role": "postgrest_test_author",-          "id": "jdoe"-        }-    -}-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 200--  it "requests without JWT token should work" $-    get "/has_count_column" `shouldRespondWith` 200
− test/Feature/AuthSpec.hs
@@ -1,179 +0,0 @@-module Feature.AuthSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import PostgREST.Config.PgVersion (PgVersion, pgVersion112)--import Protolude  hiding (get)-import SpecHelper--spec :: PgVersion -> SpecWith ((), Application)-spec actualPgVersion = describe "authorization" $ do-  let single = ("Accept","application/vnd.pgrst.object+json")--  it "denies access to tables that anonymous does not own" $-    get "/authors_only" `shouldRespondWith` (-        if actualPgVersion >= pgVersion112 then-        [json| {-          "hint":null,-          "details":null,-          "code":"42501",-          "message":"permission denied for table authors_only"} |]-           else-        [json| {-          "hint":null,-          "details":null,-          "code":"42501",-          "message":"permission denied for relation authors_only"} |]-                                            )-      { matchStatus = 401-      , matchHeaders = ["WWW-Authenticate" <:> "Bearer"]-      }--  it "denies access to tables that postgrest_test_author does not own" $-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" in-    request methodGet "/private_table" [auth] ""-      `shouldRespondWith` (-        if actualPgVersion >= pgVersion112 then-        [json| {-          "hint":null,-          "details":null,-          "code":"42501",-          "message":"permission denied for table private_table"} |]-           else-        [json| {-          "hint":null,-          "details":null,-          "code":"42501",-          "message":"permission denied for relation private_table"} |]-                          )-      { matchStatus = 403-      , matchHeaders = []-      }--  it "denies execution on functions that anonymous does not own" $-    post "/rpc/privileged_hello" [json|{"name": "anonymous"}|] `shouldRespondWith` 401--  it "allows execution on a function that postgrest_test_author owns" $-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" in-    request methodPost "/rpc/privileged_hello" [auth] [json|{"name": "jdoe"}|]-      `shouldRespondWith` [json|"Privileged hello to jdoe"|]-      { matchStatus = 200-      , matchHeaders = [matchContentTypeJson]-      }--  it "returns jwt functions as jwt tokens" $-    request methodPost "/rpc/login" [single]-      [json| { "id": "jdoe", "pass": "1234" } |]-      `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.KO-0PGp_rU-utcDBP6qwdd-Th2Fk-ICVt01I7QtTDWs"} |]-        { matchStatus = 200-        , matchHeaders = [matchContentTypeSingular]-        }--  it "sql functions can encode custom and standard claims" $-    request methodPost  "/rpc/jwt_test" [single] "{}"-      `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.G2REtPnOQMUrVRDA9OnkPJTd8R0tf4wdYOlauh1E2Ek"} |]-        { matchStatus = 200-        , matchHeaders = [matchContentTypeSingular]-        }--  it "sql functions can read custom and standard claims variables" $ do-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwfQ.V5fEpXfpb7feqwVqlcDleFdKu86bdwU2cBRT4fcMhXg"-    request methodPost "/rpc/reveal_big_jwt" [auth] "{}"-      `shouldRespondWith` [json|[{"iss":"joe","sub":"fun","exp":9999999999,"nbf":1300819380,"iat":1300819380,"jti":"foo","http://postgrest.com/foo":true}]|]--  it "allows users with permissions to see their tables" $ do-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 200--  it "works with tokens which have extra fields" $ do-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.b0eglDKYEmGi-hCvD-ddSqFl7vnDO5qkUaviaHXm3es"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 200--  -- this test will stop working 9999999999s after the UNIX EPOCH-  it "succeeds with an unexpired token" $ do-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 200--  it "fails with an expired token" $ do-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.f8__E6VQwYcDqwHmr9PG03uaZn8Zh1b0vbJ9DYS0AdM"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` [json| {"message":"JWT expired"} |]-        { matchStatus = 401-        , matchHeaders = [-            "WWW-Authenticate" <:>-            "Bearer error=\"invalid_token\", error_description=\"JWT expired\""-          ]-        }--  it "hides tables from users with invalid JWT" $ do-    let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` [json| {"message":"JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 2)"} |]-        { matchStatus = 401-        , matchHeaders = [-            "WWW-Authenticate" <:>-            "Bearer error=\"invalid_token\", error_description=\"JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 2)\""-          ]-        }--  it "should fail when jwt contains no claims" $ do-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.CUIP5V9thWsGGFsFyGijSZf1fJMfarLHI9CEJL-TGNk"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 401--  it "hides tables from users with JWT that contain no claims about role" $ do-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.RVlZDaSyKbFPvxUf3V_NQXybfRB4dlBIkAUQXVXLUAI"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 401--  it "recovers after 401 error with logged in user" $ do-    _ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |]-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"-    _ <- request methodPost "/rpc/problem" [auth] ""-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 200--  describe "custom pre-request proc acting on id claim" $ do--    it "able to switch to postgrest_test_author role (id=1)" $-      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.gKw7qI50i9hMrSJW8BlTpdMEVmMXJYxlAqueGqpa_mE" in-      request methodPost "/rpc/get_current_user" [auth]-        [json| {} |]-         `shouldRespondWith` [json|"postgrest_test_author"|]-          { matchStatus = 200-          , matchHeaders = []-          }--    it "able to switch to postgrest_test_default_role (id=2)" $-      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.nwzjMI0YLvVGJQTeoCPEBsK983b__gxdpLXisBNaO2A" in-      request methodPost "/rpc/get_current_user" [auth]-        [json| {} |]-         `shouldRespondWith` [json|"postgrest_test_default_role"|]-          { matchStatus = 200-          , matchHeaders = []-          }--    it "raises error (id=3)" $-      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.OGxEJAf60NKZiTn-tIb2jy4rqKs_ZruLGWZ40TjrJsM" in-      request methodPost "/rpc/get_current_user" [auth]-        [json| {} |]-         `shouldRespondWith` [json|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|]-          { matchStatus = 400-          , matchHeaders = []-          }--  it "allows 'Bearer' and 'bearer' as authentication schemes" $ do-    let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"-    request methodGet "/authors_only" [authHeader "Bearer" token] ""-      `shouldRespondWith` 200-    request methodGet "/authors_only" [authHeader "bearer" token] ""-      `shouldRespondWith` 200
− test/Feature/BinaryJwtSecretSpec.hs
@@ -1,21 +0,0 @@-module Feature.BinaryJwtSecretSpec where---- {{{ Imports-import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai--import Protolude-import SpecHelper--- }}}--spec :: SpecWith ((), Application)-spec = describe "server started with binary JWT secret" $--  -- this test will stop working 9999999999s after the UNIX EPOCH-  it "succeeds with jwt token encoded with a binary secret" $ do-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith` 200
− test/Feature/ConcurrentSpec.hs
@@ -1,52 +0,0 @@-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE TypeFamilies          #-}-{-# LANGUAGE UndecidableInstances  #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}-module Feature.ConcurrentSpec where--import Control.Concurrent.Async (mapConcurrently)-import Network.Wai              (Application)--import Control.Monad.Base-import Control.Monad.Trans.Control--import Network.Wai.Test        (Session)-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.Internal-import Test.Hspec.Wai.JSON--import Protolude hiding (get)--spec :: SpecWith ((), Application)-spec =-  describe "Querying in parallel" $-    it "should not raise 'transaction in progress' error" $-      raceTest 10 $-        get "/fakefake"-          `shouldRespondWith` [json|-              { "hint": null,-                "details":null,-                "code":"42P01",-                "message":"relation \"test.fakefake\" does not exist"-              } |]-          { matchStatus  = 404-          , matchHeaders = []-          }--raceTest :: Int -> WaiExpectation st -> WaiExpectation st-raceTest times = liftBaseDiscard go- where-  go test = void $ mapConcurrently (const test) [1..times]--instance MonadBaseControl IO (WaiSession st) where-  type StM (WaiSession st) a = StM Session a-  liftBaseWith f = WaiSession $-    liftBaseWith $ \runInBase ->-      f $ \k -> runInBase (unWaiSession k)-  restoreM = WaiSession . restoreM-  {-# INLINE liftBaseWith #-}-  {-# INLINE restoreM #-}--instance MonadBase IO (WaiSession st) where-  liftBase = liftIO
− test/Feature/CorsSpec.hs
@@ -1,74 +0,0 @@-module Feature.CorsSpec where---- {{{ Imports-import qualified Data.ByteString.Lazy as BL--import Network.Wai      (Application)-import Network.Wai.Test (SResponse (simpleBody, simpleHeaders))--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai--import Protolude-import SpecHelper--- }}}--spec :: SpecWith ((), Application)-spec =-  describe "CORS" $ do-    let preflightHeaders = [-          ("Accept", "*/*"),-          ("Origin", "http://example.com"),-          ("Access-Control-Request-Method", "POST"),-          ("Access-Control-Request-Headers", "Foo,Bar") ]-    let normalCors = [-          ("Host", "localhost:3000"),-          ("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0"),-          ("Origin", "http://localhost:8000"),-          ("Accept", "text/csv, */*; q=0.01"),-          ("Accept-Language", "en-US,en;q=0.5"),-          ("Accept-Encoding", "gzip, deflate"),-          ("Referer", "http://localhost:8000/"),-          ("Connection", "keep-alive") ]--    describe "preflight request" $ do-      it "replies naively and permissively to preflight request" $ do-        r <- request methodOptions "/items" preflightHeaders ""-        liftIO $ do-          let respHeaders = simpleHeaders r-          respHeaders `shouldSatisfy` matchHeader-            "Access-Control-Allow-Origin"-            "http://example.com"-          respHeaders `shouldSatisfy` matchHeader-            "Access-Control-Allow-Credentials"-            "true"-          respHeaders `shouldSatisfy` matchHeader-            "Access-Control-Allow-Methods"-            "GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD"-          respHeaders `shouldSatisfy` matchHeader-            "Access-Control-Allow-Headers"-            "Authentication, Foo, Bar, Accept, Accept-Language, Content-Language"-          respHeaders `shouldSatisfy` matchHeader-            "Access-Control-Max-Age"-            "86400"--      it "suppresses body in response" $ do-        r <- request methodOptions "/" preflightHeaders ""-        liftIO $ simpleBody r `shouldBe` ""--    describe "regular request" $-      it "exposes necesssary response headers" $ do-        r <- request methodGet "/items" [("Origin", "http://example.com")] ""-        liftIO $ simpleHeaders r `shouldSatisfy` matchHeader-          "Access-Control-Expose-Headers"-          "Content-Encoding, Content-Location, Content-Range, Content-Type, \-            \Date, Location, Server, Transfer-Encoding, Range-Unit"--    describe "postflight request" $-      it "allows INFO body through even with CORS request headers present" $ do-        r <- request methodOptions "/items" normalCors ""-        liftIO $ do-          simpleHeaders r `shouldSatisfy` matchHeader-            "Access-Control-Allow-Origin" "\\*"-          simpleBody r `shouldSatisfy` BL.null
− test/Feature/DeleteSpec.hs
@@ -1,92 +0,0 @@-module Feature.DeleteSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude hiding (get)--spec :: SpecWith ((), Application)-spec =-  describe "Deleting" $ do-    context "existing record" $ do-      it "succeeds with 204 and deletion count" $-        request methodDelete "/items?id=eq.1" [] ""-          `shouldRespondWith` ""-          { matchStatus  = 204-          , matchHeaders = ["Content-Range" <:> "*/*"]-          }--      it "returns the deleted item and count if requested" $-        request methodDelete "/items?id=eq.2" [("Prefer", "return=representation"), ("Prefer", "count=exact")] ""-          `shouldRespondWith` [json|[{"id":2}]|]-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "*/1"]-          }--      it "ignores ?select= when return not set or return=minimal" $ do-        request methodDelete "/items?id=eq.3&select=id" [] ""-          `shouldRespondWith` ""-          { matchStatus  = 204-          , matchHeaders = ["Content-Range" <:> "*/*"]-          }-        request methodDelete "/items?id=eq.3&select=id" [("Prefer", "return=minimal")] ""-          `shouldRespondWith` ""-          { matchStatus  = 204-          , matchHeaders = ["Content-Range" <:> "*/*"]-          }--      it "returns the deleted item and shapes the response" $-        request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""-          `shouldRespondWith` [json|[{"id":2,"name":"Two"}]|]-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "*/*"]-          }--      it "can rename and cast the selected columns" $-        request methodDelete "/complex_items?id=eq.3&select=ciId:id::text,ciName:name" [("Prefer", "return=representation")] ""-          `shouldRespondWith` [json|[{"ciId":"3","ciName":"Three"}]|]--      it "can embed (parent) entities" $-        request methodDelete "/tasks?id=eq.8&select=id,name,project:projects(id)" [("Prefer", "return=representation")] ""-          `shouldRespondWith` [json|[{"id":8,"name":"Code OSX","project":{"id":4}}]|]-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "*/*"]-          }--    context "known route, no records matched" $-      it "includes [] body if return=rep" $-        request methodDelete "/items?id=eq.101"-          [("Prefer", "return=representation")] ""-          `shouldRespondWith` "[]"-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "*/*"]-          }--    context "totally unknown route" $-      it "fails with 404" $-        request methodDelete "/foozle?id=eq.101" [] "" `shouldRespondWith` 404--    context "table with limited privileges" $ do-      it "fails deleting the row when return=representation and selecting all the columns" $-        request methodDelete "/app_users?id=eq.1" [("Prefer", "return=representation")] mempty-            `shouldRespondWith` 401--      it "succeeds deleting the row when return=representation and selecting only the privileged columns" $-        request methodDelete "/app_users?id=eq.1&select=id,email" [("Prefer", "return=representation")]-          [json| { "password": "passxyz" } |]-            `shouldRespondWith` [json|[ { "id": 1, "email": "test@123.com" } ]|]-            { matchStatus  = 200-            , matchHeaders = ["Content-Range" <:> "*/*"]-            }--      it "suceeds deleting the row with no explicit select when using return=minimal" $-        request methodDelete "/app_users?id=eq.2" [("Prefer", "return=minimal")] mempty-            `shouldRespondWith` 204--      it "suceeds deleting the row with no explicit select by default" $-        request methodDelete "/app_users?id=eq.3" [] mempty-            `shouldRespondWith` 204
− test/Feature/DisabledOpenApiSpec.hs
@@ -1,20 +0,0 @@-module Feature.DisabledOpenApiSpec where--import Network.HTTP.Types-import Network.Wai        (Application)--import Test.Hspec     hiding (pendingWith)-import Test.Hspec.Wai--import Protolude--spec :: SpecWith ((), Application)-spec =-  describe "Disabled OpenApi" $ do-    it "does not accept application/openapi+json and responds with 415" $-      request methodGet "/"-        [("Accept","application/openapi+json")] "" `shouldRespondWith` 415--    it "accepts application/json and responds with 404" $-      request methodGet "/"-        [("Accept","application/json")] "" `shouldRespondWith` 404
− test/Feature/EmbedDisambiguationSpec.hs
@@ -1,440 +0,0 @@-module Feature.EmbedDisambiguationSpec where--import Network.Wai (Application)--import Test.Hspec          hiding (pendingWith)-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude  hiding (get)-import SpecHelper--spec :: SpecWith ((), Application)-spec =-  describe "resource embedding disambiguation" $ do-    context "ambiguous requests that give 300 Multiple Choices" $ do-      it "errs when there's a table and view that point to the same fk" $-        get "/message?select=id,body,sender(name,sent)" `shouldRespondWith`-          [json|-            {-              "details": [-                {-                    "cardinality": "many-to-one",-                    "relationship": "message_sender_fkey[sender][id]",-                    "embedding": "message with person"-                },-                {-                    "cardinality": "many-to-one",-                    "relationship": "message_sender_fkey[sender][id]",-                    "embedding": "message with person_detail"-                }-              ],-              "hint": "Try changing 'sender' to one of the following: 'person!message_sender_fkey', 'person_detail!message_sender_fkey'. Find the desired relationship in the 'details' key.",-              "message": "Could not embed because more than one relationship was found for 'message' and 'sender'"-            }-          |]-          { matchStatus  = 300-          , matchHeaders = [matchContentTypeJson]-          }--      it "errs when there are o2m and m2m cardinalities to the target table" $-        get "/sites?select=*,big_projects(*)" `shouldRespondWith`-          [json|-            {-              "details": [-                {-                  "cardinality": "many-to-one",-                  "relationship": "main_project[main_project_id][big_project_id]",-                  "embedding": "sites with big_projects"-                },-                {-                  "cardinality": "many-to-many",-                  "relationship": "test.jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",-                  "embedding": "sites with big_projects"-                },-                {-                  "cardinality": "many-to-many",-                  "relationship": "test.main_jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",-                  "embedding": "sites with big_projects"-                }-              ],-              "hint": "Try changing 'big_projects' to one of the following: 'big_projects!main_project', 'big_projects!jobs', 'big_projects!main_jobs'. Find the desired relationship in the 'details' key.",-              "message": "Could not embed because more than one relationship was found for 'sites' and 'big_projects'"-            }-          |]-          { matchStatus  = 300-          , matchHeaders = [matchContentTypeJson]-          }--      it "errs on an ambiguous embed that has a circular reference" $-        get "/agents?select=*,departments(*)" `shouldRespondWith`-          [json|-            {-              "details": [-                {-                    "cardinality": "many-to-one",-                    "relationship": "agents_department_id_fkey[department_id][id]",-                    "embedding": "agents with departments"-                },-                {-                    "cardinality": "one-to-many",-                    "relationship": "departments_head_id_fkey[id][head_id]",-                    "embedding": "agents with departments"-                }-              ],-              "hint": "Try changing 'departments' to one of the following: 'departments!agents_department_id_fkey', 'departments!departments_head_id_fkey'. Find the desired relationship in the 'details' key.",-              "message": "Could not embed because more than one relationship was found for 'agents' and 'departments'"-            }-           |]-          { matchStatus  = 300-          , matchHeaders = [matchContentTypeJson]-          }--      it "errs when there are more than two fks on a junction table(currently impossible to disambiguate, only choice is to split the table)" $-        -- We have 4 possibilities for doing the junction JOIN here.-        -- This could be solved by specifying two additional fks, like whatev_projects!fk1!fk2(*)-        -- If the need arises this capability can be added later without causing a breaking change-        get "/whatev_sites?select=*,whatev_projects(*)" `shouldRespondWith`-          [json|-            {-              "details": [-                {-                  "cardinality": "many-to-many",-                  "relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_1_fkey]",-                  "embedding": "whatev_sites with whatev_projects"-                },-                {-                  "cardinality": "many-to-many",-                  "relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_2_fkey]",-                  "embedding": "whatev_sites with whatev_projects"-                },-                {-                  "cardinality": "many-to-many",-                  "relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_1_fkey]",-                  "embedding": "whatev_sites with whatev_projects"-                },-                {-                  "cardinality": "many-to-many",-                  "relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_2_fkey]",-                  "embedding": "whatev_sites with whatev_projects"-                }-              ],-              "hint": "Try changing 'whatev_projects' to one of the following: 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs'. Find the desired relationship in the 'details' key.",-              "message": "Could not embed because more than one relationship was found for 'whatev_sites' and 'whatev_projects'"-            }-          |]-          { matchStatus  = 300-          , matchHeaders = [matchContentTypeJson]-          }--    context "disambiguating requests with embed hints" $ do--      context "using FK to specify the relationship" $ do-        it "can embed by FK name" $-          get "/projects?id=in.(1,3)&select=id,name,client(id,name)" `shouldRespondWith`-            [json|[{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client":{"id":2,"name":"Apple"}}]|]-            { matchHeaders = [matchContentTypeJson] }--        it "can embed by FK name and select the FK column at the same time" $-          get "/projects?id=in.(1,3)&select=id,name,client_id,client(id,name)" `shouldRespondWith`-            [json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]-            { matchHeaders = [matchContentTypeJson] }--        it "can embed parent with view!fk and grandparent by using fk" $-          get "/tasks?id=eq.1&select=id,name,projects_view!project(id,name,client(id,name))" `shouldRespondWith`-            [json|[{"id":1,"name":"Design w7","projects_view":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]--        it "can embed by using a composite FK name" $-          get "/unit_workdays?select=unit_id,day,fst_shift(car_id,schedule(name)),snd_shift(camera_id,schedule(name))" `shouldRespondWith`-            [json| [-              {-                "day": "2019-12-02",-                "fst_shift": {-                    "car_id": "CAR-349",-                    "schedule": {-                        "name": "morning"-                    }-                },-                "snd_shift": {-                    "camera_id": "CAM-123",-                    "schedule": {-                        "name": "night"-                    }-                },-                "unit_id": 1-              }-            ] |]-            { matchHeaders = [matchContentTypeJson] }--        it "embeds by using two fks pointing to the same table" $-          get "/orders?id=eq.1&select=id, name, billing(address), shipping(address)" `shouldRespondWith`-            [json|[{"id":1,"name":"order 1","billing":{"address": "address 1"},"shipping":{"address": "address 2"}}]|]-            { matchHeaders = [matchContentTypeJson] }--        it "fails if the fk is not known" $-          get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`-            [json|{-              "hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",-              "message":"Could not find a relationship between message and person in the schema cache"}|]-            { matchStatus = 400-            , matchHeaders = [matchContentTypeJson] }--        it "can request a parent with fk" $-          get "/comments?select=content,user(name)" `shouldRespondWith`-            [json|[ { "content": "Needs to be delivered ASAP", "user": { "name": "Angela Martin" } } ]|]-            { matchHeaders = [matchContentTypeJson] }--        it "can request two parents with fks" $-          get "/articleStars?select=createdAt,article(id),user(name)&limit=1"-            `shouldRespondWith`-              [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"id": 1},"user":{"name": "Angela Martin"}}]|]--        it "can specify a view!fk" $-          get "/message?select=id,body,sender:person_detail!message_sender_fkey(name,sent),recipient:person_detail!message_recipient_fkey(name,received)&id=lt.4" `shouldRespondWith`-            [json|-              [{"id":1,"body":"Hello Jane","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}},-               {"id":2,"body":"Hi John","sender":{"name":"Jane","sent":1},"recipient":{"name":"John","received":1}},-               {"id":3,"body":"How are you doing?","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}}] |]-            { matchHeaders = [matchContentTypeJson] }--        it "can specify a table!fk hint and request children 2 levels" $-          get "/clients?id=eq.1&select=id,projects:projects!client(id,tasks(id))" `shouldRespondWith`-            [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]-            { matchHeaders = [matchContentTypeJson] }--        it "can disambiguate with the fk in case of an o2m and m2m relationship to the same table" $-          get "/sites?select=name,main_project(name)&site_id=eq.1" `shouldRespondWith`-            [json| [ { "name": "site 1", "main_project": { "name": "big project 1" } } ] |]-            { matchHeaders = [matchContentTypeJson] }--      context "using the column name of the FK to specify the relationship" $ do-        it "can embed by column" $-          get "/projects?id=in.(1,3)&select=id,name,client_id(id,name)" `shouldRespondWith`-            [json|[{"id":1,"name":"Windows 7","client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":{"id":2,"name":"Apple"}}]|]-            { matchHeaders = [matchContentTypeJson] }--        it "can embed by column and select the column at the same time, if aliased" $-          get "/projects?id=in.(1,3)&select=id,name,client_id,client:client_id(id,name)" `shouldRespondWith`-            [json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]-            { matchHeaders = [matchContentTypeJson] }--        it "can embed parent by using view!column and grandparent by using the column" $-          get "/tasks?id=eq.1&select=id,name,project:projects_view!project_id(id,name,client:client_id(id,name))" `shouldRespondWith`-            [json|[{"id":1,"name":"Design w7","project":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]--        it "can specify table!column" $-          get "/message?select=id,body,sender:person!sender(name),recipient:person!recipient(name)&id=lt.4" `shouldRespondWith`-            [json|-              [{"id":1,"body":"Hello Jane","sender":{"name":"John"},"recipient":{"name":"Jane"}},-               {"id":2,"body":"Hi John","sender":{"name":"Jane"},"recipient":{"name":"John"}},-               {"id":3,"body":"How are you doing?","sender":{"name":"John"},"recipient":{"name":"Jane"}}] |]-            { matchHeaders = [matchContentTypeJson] }--        it "will embed using a column that has uppercase chars" $-          get "/ghostBusters?select=escapeId(*)" `shouldRespondWith`-            [json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]-            { matchHeaders = [matchContentTypeJson] }--        it "embeds by using two columns pointing to the same table" $-          get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping_address_id(id)" `shouldRespondWith`-            [json|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping_address_id":{"id":2}}]|]-            { matchHeaders = [matchContentTypeJson] }--        it "can disambiguate with the column in case of an o2m and m2m relationship to the same table" $-          get "/sites?select=name,main_project_id(name)&site_id=eq.1" `shouldRespondWith`-            [json| [ { "name": "site 1", "main_project_id": { "name": "big project 1" } } ] |]-            { matchHeaders = [matchContentTypeJson] }--      context "using the junction to disambiguate the request" $-        it "can specify the junction of an m2m relationship" $ do-          get "/sites?select=*,big_projects!jobs(name)&site_id=in.(1,2)" `shouldRespondWith`-            [json|-              [-                {-                  "big_projects": [-                    {-                      "name": "big project 1"-                    }-                  ],-                  "main_project_id": 1,-                  "name": "site 1",-                  "site_id": 1-                },-                {-                  "big_projects": [-                    {-                      "name": "big project 1"-                    },-                    {-                      "name": "big project 2"-                    }-                  ],-                  "main_project_id": null,-                  "name": "site 2",-                  "site_id": 2-                }-              ]-            |]-          get "/sites?select=*,big_projects!main_jobs(name)&site_id=in.(1,2)" `shouldRespondWith`-            [json|-              [-                {-                  "big_projects": [-                    {-                      "name": "big project 1"-                    }-                  ],-                  "main_project_id": 1,-                  "name": "site 1",-                  "site_id": 1-                },-                {-                  "big_projects": [],-                  "main_project_id": null,-                  "name": "site 2",-                  "site_id": 2-                }-              ]-            |]-            { matchHeaders = [matchContentTypeJson] }--      context "using a FK column and a FK to specify the relationship" $-        it "embeds by using a column and a fk pointing to the same table" $-          get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping(id)" `shouldRespondWith`-            [json|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping":{"id":2}}]|]-            { matchHeaders = [matchContentTypeJson] }--    context "tables with self reference foreign keys" $ do-      context "one self reference foreign key" $ do-        it "embeds parents recursively" $-          get "/family_tree?id=in.(3,4)&select=id,parent(id,name,parent(*))" `shouldRespondWith`-            [json|[-              { "id": "3", "parent": { "id": "1", "name": "Parental Unit", "parent": null } },-              { "id": "4", "parent": { "id": "2", "name": "Kid One", "parent": { "id": "1", "name": "Parental Unit", "parent": null } } }-            ]|]-            { matchHeaders = [matchContentTypeJson] }--        it "embeds children recursively" $-          get "/family_tree?id=eq.1&select=id,name, children:family_tree!parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`-            [json|[{-              "id": "1", "name": "Parental Unit", "children": [-                { "id": "2", "name": "Kid One", "children": [ { "id": "4", "name": "Grandkid One" } ] },-                { "id": "3", "name": "Kid Two", "children": [ { "id": "5", "name": "Grandkid Two" } ] }-              ]-            }]|] { matchHeaders = [matchContentTypeJson] }--        it "embeds parent and then embeds children" $-          get "/family_tree?id=eq.2&select=id,name,parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`-            [json|[{-              "id": "2", "name": "Kid One", "parent": {-                "id": "1", "name": "Parental Unit", "children": [ { "id": "2", "name": "Kid One" }, { "id": "3", "name": "Kid Two"} ]-              }-            }]|] { matchHeaders = [matchContentTypeJson] }--      context "two self reference foreign keys" $ do-        it "embeds parents" $-          get "/organizations?select=id,name,referee(id,name),auditor(id,name)&id=eq.3" `shouldRespondWith`-            [json|[{-              "id": 3, "name": "Acme",-              "referee": {-                "id": 1,-                "name": "Referee Org"-              },-              "auditor": {-                "id": 2,-                "name": "Auditor Org"-              }-            }]|] { matchHeaders = [matchContentTypeJson] }--        it "embeds children" $ do-          get "/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1" `shouldRespondWith`-            [json|[{-              "id": 1, "name": "Referee Org",-              "refereeds": [-                {-                  "id": 3,-                  "name": "Acme"-                },-                {-                  "id": 4,-                  "name": "Umbrella"-                }-              ]-            }]|] { matchHeaders = [matchContentTypeJson] }-          get "/organizations?select=id,name,auditees:organizations!auditor(id,name)&id=eq.2" `shouldRespondWith`-            [json|[{-              "id": 2, "name": "Auditor Org",-              "auditees": [-                {-                  "id": 3,-                  "name": "Acme"-                },-                {-                  "id": 4,-                  "name": "Umbrella"-                }-              ]-            }]|] { matchHeaders = [matchContentTypeJson] }--        it "embeds other relations(manager) besides the self reference" $ do-          get "/organizations?select=name,manager(name),referee(name,manager(name),auditor(name,manager(name))),auditor(name,manager(name),referee(name,manager(name)))&id=eq.5" `shouldRespondWith`-            [json|[{-              "name":"Cyberdyne",-              "manager":{"name":"Cyberdyne Manager"},-              "referee":{-                "name":"Acme",-                "manager":{"name":"Acme Manager"},-                "auditor":{-                  "name":"Auditor Org",-                  "manager":{"name":"Auditor Manager"}}},-              "auditor":{-                "name":"Umbrella",-                "manager":{"name":"Umbrella Manager"},-                "referee":{-                  "name":"Referee Org",-                  "manager":{"name":"Referee Manager"}}}-            }]|] { matchHeaders = [matchContentTypeJson] }--          get "/organizations?select=name,manager(name),auditees:organizations!auditor(name,manager(name),refereeds:organizations!referee(name,manager(name)))&id=eq.2" `shouldRespondWith`-            [json|[{-              "name":"Auditor Org",-              "manager":{"name":"Auditor Manager"},-              "auditees":[-                {"name":"Acme",-                 "manager":{"name":"Acme Manager"},-                 "refereeds":[-                   {"name":"Cyberdyne",-                    "manager":{"name":"Cyberdyne Manager"}},-                   {"name":"Oscorp",-                    "manager":{"name":"Oscorp Manager"}}]},-                {"name":"Umbrella",-                 "manager":{"name":"Umbrella Manager"},-                 "refereeds":[]}]-            }]|] { matchHeaders = [matchContentTypeJson] }--    context "m2m embed when there's a junction in an internal schema" $ do-      -- https://github.com/PostgREST/postgrest/issues/1736-      it "works with no ambiguity when there's an exposed view of the junction" $ do-        get "/screens?select=labels(name)" `shouldRespondWith`-          [json|[{"labels":[{"name":"fruit"}]}, {"labels":[{"name":"vehicles"}]}, {"labels":[{"name":"vehicles"}, {"name":"fruit"}]}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/actors?select=*,films(*)" `shouldRespondWith`-          [json|[ {"id":1,"name":"john","films":[{"id":12,"title":"douze commandements"}]},-                  {"id":2,"name":"mary","films":[{"id":2001,"title":"odyssée de l'espace"}]}]|]-          { matchHeaders = [matchContentTypeJson] }-      it "doesn't work if the junction is only internal" $-        get "/end_1?select=end_2(*)" `shouldRespondWith`-          [json|{-            "hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",-            "message":"Could not find a relationship between end_1 and end_2 in the schema cache"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson] }-      it "shouldn't try to embed if the private junction has an exposed homonym" $-        -- ensures the "invalid reference to FROM-clause entry for table "rollen" error doesn't happen.-        -- Ref: https://github.com/PostgREST/postgrest/issues/1587#issuecomment-734995669-        get "/schauspieler?select=filme(*)" `shouldRespondWith`-          [json|{-            "hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",-            "message":"Could not find a relationship between schauspieler and filme in the schema cache"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson] }
− test/Feature/EmbedInnerJoinSpec.hs
@@ -1,356 +0,0 @@-module Feature.EmbedInnerJoinSpec where--import Network.HTTP.Types-import Network.Wai        (Application)--import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude  hiding (get)-import SpecHelper--spec :: SpecWith ((), Application)-spec =-  describe "Embedding with an inner join" $ do-    context "many-to-one relationships" $ do-      it "ignores null embeddings while the default left join doesn't" $ do-        get "/projects?select=id,clients!inner(id)" `shouldRespondWith`-          [json|[-            {"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}},-            {"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/projects?select=id,clients!left(id)" `shouldRespondWith`-          [json|[-            {"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}},-            {"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}},-            {"id":5,"clients":null}]|]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/projects?select=id,clients!inner(id)" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-3/4" ]-          }--      it "filters source tables when the embedded table is filtered" $ do-        get "/projects?select=id,clients!inner(id)&clients.id=eq.1" `shouldRespondWith`-          [json|[-            {"id":1,"clients":{"id":1}},-            {"id":2,"clients":{"id":1}}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/projects?select=id,clients!inner(id)&clients.id=eq.2" `shouldRespondWith`-          [json|[-            {"id":3,"clients":{"id":2}},-            {"id":4,"clients":{"id":2}}] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/projects?select=id,clients!inner(id)&clients.id=eq.0" `shouldRespondWith`-          [json|[]|]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/projects?select=id,clients!inner(id)&clients.id=eq.1" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-1/2" ]-          }--      it "filters source tables when a two levels below embedded table is filtered" $ do-        get "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.1" `shouldRespondWith`-          [json|[-            {"id":1,"projects":{"id":1,"clients":{"id":1}}},-            {"id":2,"projects":{"id":1,"clients":{"id":1}}},-            {"id":3,"projects":{"id":2,"clients":{"id":1}}},-            {"id":4,"projects":{"id":2,"clients":{"id":1}}}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.2" `shouldRespondWith`-          [json|[-            {"id":5,"projects":{"id":3,"clients":{"id":2}}},-            {"id":6,"projects":{"id":3,"clients":{"id":2}}},-            {"id":7,"projects":{"id":4,"clients":{"id":2}}},-            {"id":8,"projects":{"id":4,"clients":{"id":2}}}]|]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.1" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-3/4" ]-          }--      it "only affects the source table rows if his direct embedding is an inner join" $ do-        get "/tasks?select=id,projects(id,clients!inner(id))&projects.clients.id=eq.2" `shouldRespondWith`-          [json|[-            {"id":1,"projects":null},-            {"id":2,"projects":null},-            {"id":3,"projects":null},-            {"id":4,"projects":null},-            {"id":5,"projects":{"id":3,"clients":{"id":2}}},-            {"id":6,"projects":{"id":3,"clients":{"id":2}}},-            {"id":7,"projects":{"id":4,"clients":{"id":2}}},-            {"id":8,"projects":{"id":4,"clients":{"id":2}}}]|]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/tasks?select=id,projects(id,clients!inner(id))&projects.clients.id=eq.2" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-7/8" ]-          }--      it "works with views" $ do-        get "/books?select=title,authors!inner(name)&authors.name=eq.George%20Orwell" `shouldRespondWith`-          [json| [{"title":"1984","authors":{"name":"George Orwell"}}] |]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/books?select=title,authors!inner(name)&authors.name=eq.George%20Orwell" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-0/1" ]-          }--    context "one-to-many relationships" $ do-      it "ignores empty array embeddings while the default left join doesn't" $ do-        get "/entities?select=id,child_entities!inner(id)" `shouldRespondWith`-          [json|[-            {"id":1,"child_entities":[{"id":1}, {"id":2}, {"id":4}, {"id":5}]},-            {"id":2,"child_entities":[{"id":3}, {"id":6}]}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/entities?select=id,child_entities!left(id)" `shouldRespondWith`-          [json| [-            {"id":1,"child_entities":[{"id":1}, {"id":2}, {"id":4}, {"id":5}]},-            {"id":2,"child_entities":[{"id":3}, {"id":6}]},-            {"id":3,"child_entities":[]},-            {"id":4,"child_entities":[]}] |]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/entities?select=id,child_entities!inner(id)" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-1/2" ]-          }--      it "filters source tables when the embedded table is filtered" $ do-        get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.1" `shouldRespondWith`-          [json|[{"id":1,"child_entities":[{"id":1}]}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.3" `shouldRespondWith`-          [json|[{"id":2,"child_entities":[{"id":3}]}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.0" `shouldRespondWith`-          [json|[]|]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.1" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-0/1" ]-          }--      it "filters source tables when a two levels below embedded table is filtered" $ do-        get "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=in.(1,5)"-          `shouldRespondWith`-          [json|[-            {-              "id": 1,-              "child_entities": [-                { "id": 1, "grandchild_entities": [ { "id": 1 } ] },-                { "id": 2, "grandchild_entities": [ { "id": 5 } ] }]-            }-          ]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=eq.2" `shouldRespondWith`-          [json|[-            {-              "id": 1,-              "child_entities": [-                { "id": 1, "grandchild_entities": [ { "id": 2 } ] } ]-            }-          ]|]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=in.(1,5)" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-0/1" ]-          }--      it "only affects the source table rows if his direct embedding is an inner join" $ do-        get "/entities?select=id,child_entities!inner(id,grandchild_entities(id))&child_entities.grandchild_entities.id=eq.2" `shouldRespondWith`-          [json|[-            {-              "id": 1,-              "child_entities": [-                { "id": 1, "grandchild_entities": [ { "id": 2 } ] },-                { "id": 2, "grandchild_entities": [] },-                { "id": 4, "grandchild_entities": [] },-                { "id": 5, "grandchild_entities": [] } ]-            },-            {-              "id": 2,-              "child_entities": [-                { "id": 3, "grandchild_entities": [] },-                { "id": 6, "grandchild_entities": [] } ]-            }-          ]|]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/entities?select=id,child_entities!inner(id,grandchild_entities(id))&child_entities.grandchild_entities.id=eq.2" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-1/2" ]-          }--      it "works with views" $ do-        get "/authors?select=*,books!inner(*)&books.title=eq.1984" `shouldRespondWith`-          [json| [{"id":1,"name":"George Orwell","books":[{"id":1,"title":"1984","publication_year":1949,"author_id":1}]}] |]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/authors?select=*,books!inner(*)&books.title=eq.1984" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-0/1" ]-          }--    context "many-to-many relationships" $ do-      it "ignores empty array embeddings while the default left join doesn't" $ do-        get "/products?select=id,suppliers!inner(id)" `shouldRespondWith`-          [json| [-            {"id":1,"suppliers":[{"id":1}, {"id":2}]},-            {"id":2,"suppliers":[{"id":1}, {"id":3}]}] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/products?select=id,suppliers!left(id)" `shouldRespondWith`-          [json| [-            {"id":1,"suppliers":[{"id":1}, {"id":2}]},-            {"id":2,"suppliers":[{"id":1}, {"id":3}]},-            {"id":3,"suppliers":[]}] |]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/products?select=id,suppliers!inner(id)" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-1/2" ]-          }--      it "filters source tables when the embedded table is filtered" $ do-        get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.2" `shouldRespondWith`-          [json| [{"id":1,"suppliers":[{"id":2}]}] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.3" `shouldRespondWith`-          [json| [{"id":2,"suppliers":[{"id":3}]}] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.0" `shouldRespondWith`-          [json| [] |]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/products?select=id,suppliers!inner(id)&suppliers.id=eq.2" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-0/1" ]-          }--      it "filters source tables when a two levels below embedded table is filtered" $ do-        get "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.3"-          `shouldRespondWith`-          [json|[{"id":1,"suppliers":[{"id":2,"trade_unions":[{"id":3}]}]}] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.4"-          `shouldRespondWith`-          [json|[{"id":1,"suppliers":[{"id":2,"trade_unions":[{"id":4}]}]}] |]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.3" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-0/1" ]-          }--      it "only affects the source table rows if his direct embedding is an inner join" $ do-        get "/products?select=id,suppliers!inner(id,trade_unions(id))&suppliers.trade_unions.id=eq.3" `shouldRespondWith`-          [json|[-            {"id":1,"suppliers":[{"id":1,"trade_unions":[]}, {"id":2,"trade_unions":[{"id":3}]}]},-            {"id":2,"suppliers":[{"id":1,"trade_unions":[]}, {"id":3,"trade_unions":[]}]}]|]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/products?select=id,suppliers!inner(id,trade_unions(id))&suppliers.trade_unions.id=eq.3" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-1/2" ]-          }--      it "works with views" $ do-        get "/actors?select=*,films!inner(*)&films.title=eq.douze%20commandements" `shouldRespondWith`-          [json| [{"id":1,"name":"john","films":[{"id":12,"title":"douze commandements"}]}] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/films?select=*,actors!inner(*)&actors.name=eq.john" `shouldRespondWith`-          [json| [{"id":12,"title":"douze commandements","actors":[{"id":1,"name":"john"}]}] |]-          { matchHeaders = [matchContentTypeJson] }-        request methodHead "/actors?select=*,films!inner(*)&films.title=eq.douze%20commandements" [("Prefer", "count=exact")] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson-                           , "Content-Range" <:> "0-0/1" ]-          }--    it "works with m2o and m2m relationships combined" $ do-      get "/projects?select=name,clients!inner(name),users!inner(name)" `shouldRespondWith`-        [json| [-          {"name":"Windows 7","clients":{"name":"Microsoft"},"users":[{"name":"Angela Martin"}, {"name":"Dwight Schrute"}]},-          {"name":"Windows 10","clients":{"name":"Microsoft"},"users":[{"name":"Angela Martin"}]},-          {"name":"IOS","clients":{"name":"Apple"},"users":[{"name":"Michael Scott"}, {"name":"Dwight Schrute"}]},-          {"name":"OSX","clients":{"name":"Apple"},"users":[{"name":"Michael Scott"}]}]|]-        { matchHeaders = [matchContentTypeJson] }-      request methodHead "/projects?select=name,clients!inner(name),users!inner(name)" [("Prefer", "count=exact")] mempty-        `shouldRespondWith` ""-        { matchStatus  = 200-        , matchHeaders = [ matchContentTypeJson-                         , "Content-Range" <:> "0-3/4" ]-        }--    it "works with rpc" $ do-      get "/rpc/getallprojects?select=id,clients!inner(id)&clients.id=eq.1" `shouldRespondWith`-        [json| [{"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}}] |]-        { matchHeaders = [matchContentTypeJson] }-      request methodHead "/rpc/getallprojects?select=id,clients!inner(id)&clients.id=eq.1" [("Prefer", "count=exact")] mempty-        `shouldRespondWith` ""-        { matchStatus  = 200-        , matchHeaders = [ matchContentTypeJson-                         , "Content-Range" <:> "0-1/2" ]-        }--    it "works when using hints" $ do-      get "/projects?select=id,clients!client!inner(id)&clients.id=eq.2" `shouldRespondWith`-        [json| [{"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}}] |]-        { matchHeaders = [matchContentTypeJson] }-      get "/projects?select=id,client!inner(id)&client.id=eq.2" `shouldRespondWith`-        [json| [{"id":3,"client":{"id":2}}, {"id":4,"client":{"id":2}}] |]-        { matchHeaders = [matchContentTypeJson] }-      request methodHead "/projects?select=id,clients!client!inner(id)&clients.id=eq.2" [("Prefer", "count=exact")] mempty-        `shouldRespondWith` ""-        { matchStatus  = 200-        , matchHeaders = [ matchContentTypeJson-                         , "Content-Range" <:> "0-1/2" ]-        }--    it "works with many one-to-many relationships" $ do-      -- https://github.com/PostgREST/postgrest/issues/1977-      get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)" `shouldRespondWith`-        [json|[-          {"id":1,"name":"Walmart","contact":[{"name":"Wally Walton"}, {"name":"Wilma Wellers"}],"clientinfo":[{"other":"123 Main St"}]},-          {"id":2,"name":"Target", "contact":[{"name":"Tabby Targo"}],"clientinfo":[{"other":"456 South 3rd St"}]},-          {"id":3,"name":"Big Lots","contact":[{"name":"Bobby Bots"}, {"name":"Bonnie Bits"}, {"name":"Billy Boats"}],"clientinfo":[{"other":"789 Palm Tree Ln"}]}-        ]|]-        { matchHeaders = [matchContentTypeJson] }-      get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)&contact.name=eq.Wally%20Walton" `shouldRespondWith`-        [json|[-          {"id":1,"name":"Walmart","contact":[{"name":"Wally Walton"}],"clientinfo":[{"other":"123 Main St"}]}-        ]|]-        { matchHeaders = [matchContentTypeJson] }-      get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)&clientinfo.other=eq.456%20South%203rd%20St" `shouldRespondWith`-        [json|[-          {"id":2,"name":"Target","clientinfo":[{"other":"456 South 3rd St"}],"contact":[{"name":"Tabby Targo"}]}-        ]|]-        { matchHeaders = [matchContentTypeJson] }-      request methodHead "/client?select=id,name,contact!inner(name),clientinfo!inner(other)" [("Prefer", "count=exact")] mempty-        `shouldRespondWith` ""-        { matchStatus  = 200-        , matchHeaders = [ matchContentTypeJson-                         , "Content-Range" <:> "0-2/3" ]-        }
− test/Feature/ExtraSearchPathSpec.hs
@@ -1,39 +0,0 @@-module Feature.ExtraSearchPathSpec where--import Network.HTTP.Types-import Network.Wai         (Application)-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude  hiding (get)-import SpecHelper--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] }--  it "can detect fk relations through multiple views recursively when middle views are in extra search path" $-    get "/consumers_extra_view?select=*,orders_view(*)" `shouldRespondWith` 200
− test/Feature/HtmlRawOutputSpec.hs
@@ -1,30 +0,0 @@-module Feature.HtmlRawOutputSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec         hiding (pendingWith)-import Test.Hspec.Wai-import Text.Heredoc--import Protolude  hiding (get)-import SpecHelper (acceptHdrs)--spec :: SpecWith ((), Application)-spec = describe "When raw-media-types is set to \"text/html\"" $-  it "can get raw output with Accept: text/html" $-    request methodGet "/rpc/welcome.html" (acceptHdrs "text/html") ""-      `shouldRespondWith`-      [str|-          |<html>-          |  <head>-          |    <title>PostgREST</title>-          |  </head>-          |  <body>-          |    <h1>Welcome to PostgREST</h1>-          |  </body>-          |</html>-          |]-      { matchStatus = 200-      , matchHeaders = ["Content-Type" <:> "text/html"]-      }
− test/Feature/IgnorePrivOpenApiSpec.hs
@@ -1,76 +0,0 @@-module Feature.IgnorePrivOpenApiSpec where--import Control.Lens ((^?))--import Data.Aeson.Lens-import Data.Aeson.QQ--import Network.HTTP.Types-import Network.Wai        (Application)-import Network.Wai.Test   (SResponse (..))--import Test.Hspec     hiding (pendingWith)-import Test.Hspec.Wai--import Protolude  hiding (get)-import SpecHelper--spec :: SpecWith ((), Application)-spec = describe "OpenAPI Ignore Privileges" $ do-  it "root path returns a valid openapi spec" $ do-    validateOpenApiResponse [("Accept", "application/openapi+json")]-    request methodHead "/" (acceptHdrs "application/openapi+json") ""-      `shouldRespondWith` "" { matchStatus  = 200 }--  describe "table" $ do--    it "includes privileged table even if user does not have permission" $ do-      r <- simpleBody <$> get "/"-      let tableTag = r ^? key "paths" . key "/authors_only"-                     . key "post" . key "tags"-                     . nth 0--      liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|]--    it "only includes tables that belong to another schema if the Accept-Profile header is used" $ do-      r1 <- simpleBody <$> get "/"-      let tableKey1 = r1 ^? key "paths" . key "/children"--      liftIO $ tableKey1 `shouldBe` Nothing--      r2 <- simpleBody <$> request methodGet "/" [("Accept-Profile", "v1")] ""-      let tableKey2 = r2 ^? key "paths" . key "/children"--      liftIO $ tableKey2 `shouldNotBe` Nothing--    it "includes comments on tables" $ do-      r <- simpleBody <$> get "/"--      let grandChildGet s = key "paths" . key "/grandchild_entities" . key "get" . key s-          grandChildGetSummary = r ^? grandChildGet "summary"-          grandChildGetDescription = r ^? grandChildGet "description"--      liftIO $ do-        grandChildGetSummary `shouldBe` Just "grandchild_entities summary"-        grandChildGetDescription `shouldBe` Just "grandchild_entities description\nthat spans\nmultiple lines"--  describe "RPC" $ do--    it "includes privileged function even if user does not have permission" $ do-      r <- simpleBody <$> get "/"-      let funcTag = r ^? key "paths" . key "/rpc/privileged_hello"-                    . key "post" . key "tags"-                    . nth 0--      liftIO $ funcTag `shouldBe` Just [aesonQQ|"(rpc) privileged_hello"|]--    it "only includes functions that belong to another schema if the Accept-Profile header is used" $ do-      r1 <- simpleBody <$> get "/"-      let funcKey1 = r1 ^? key "paths" . key "/rpc/get_parents_below"--      liftIO $ funcKey1 `shouldBe` Nothing--      r2 <- simpleBody <$> request methodGet "/" [("Accept-Profile", "v1")] ""-      let funcKey2 = r2 ^? key "paths" . key "/rpc/get_parents_below"--      liftIO $ funcKey2 `shouldNotBe` Nothing
− test/Feature/InsertSpec.hs
@@ -1,558 +0,0 @@-module Feature.InsertSpec where--import Data.List              (lookup)-import Network.Wai            (Application)-import Network.Wai.Test       (SResponse (simpleHeaders))-import Test.Hspec             hiding (pendingWith)-import Test.Hspec.Wai.Matcher (bodyEquals)--import Network.HTTP.Types-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON-import Text.Heredoc--import PostgREST.Config.PgVersion (PgVersion, pgVersion110,-                                   pgVersion112, pgVersion130)--import Protolude  hiding (get)-import SpecHelper--spec :: PgVersion -> SpecWith ((), Application)-spec actualPgVersion = do-  describe "Posting new record" $ do-    context "disparate json types" $ do-      it "accepts disparate json types" $ do-        post "/menagerie"-          [json| {-            "integer": 13, "double": 3.14159, "varchar": "testing!"-          , "boolean": false, "date": "1900-01-01", "money": "$3.99"-          , "enum": "foo"-          } |] `shouldRespondWith` ""-          { matchStatus  = 201-            -- should not have content type set when body is empty-          , matchHeaders = [matchHeaderAbsent hContentType]-          }--      it "filters columns in result using &select" $-        request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=representation")]-          [json| [{-            "integer": 14, "double": 3.14159, "varchar": "testing!"-          , "boolean": false, "date": "1900-01-01", "money": "$3.99"-          , "enum": "foo"-          }] |] `shouldRespondWith` [json|[{"integer":14,"varchar":"testing!"}]|]-          { matchStatus  = 201-          , matchHeaders = [matchContentTypeJson]-          }--      it "ignores &select when return not set or using return=minimal" $ do-        request methodPost "/menagerie?select=integer,varchar" []-          [json| [{-            "integer": 15, "double": 3.14159, "varchar": "testing!"-          , "boolean": false, "date": "1900-01-01", "money": "$3.99"-          , "enum": "foo"-          }] |] `shouldRespondWith` ""-          { matchStatus  = 201 }-        request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=minimal")]-          [json| [{-            "integer": 16, "double": 3.14159, "varchar": "testing!"-          , "boolean": false, "date": "1900-01-01", "money": "$3.99"-          , "enum": "foo"-          }] |] `shouldRespondWith` ""-          { matchStatus  = 201 }--    context "non uniform json array" $ do-      it "rejects json array that isn't exclusivily composed of objects" $-        post "/articles"-             [json| [{"id": 100, "body": "xxxxx"}, 123, "xxxx", {"id": 111, "body": "xxxx"}] |]-        `shouldRespondWith`-             [json| {"message":"All object keys must match"} |]-             { matchStatus  = 400-             , matchHeaders = [matchContentTypeJson]-             }--      it "rejects json array that has objects with different keys" $-        post "/articles"-             [json| [{"id": 100, "body": "xxxxx"}, {"id": 111, "body": "xxxx", "owner": "me"}] |]-        `shouldRespondWith`-             [json| {"message":"All object keys must match"} |]-             { matchStatus  = 400-             , matchHeaders = [matchContentTypeJson]-             }--    context "requesting full representation" $ do-      it "includes related data after insert" $-        request methodPost "/projects?select=id,name,clients(id,name)"-                [("Prefer", "return=representation"), ("Prefer", "count=exact")]-          [json|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` [json|[{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}]|]-          { matchStatus  = 201-          , matchHeaders = [ matchContentTypeJson-                           , "Location" <:> "/projects?id=eq.6"-                           , "Content-Range" <:> "*/1" ]-          }--      it "can rename and cast the selected columns" $-        request methodPost "/projects?select=pId:id::text,pName:name,cId:client_id::text"-                [("Prefer", "return=representation")]-          [json|{"id":7,"name":"New Project","client_id":2}|] `shouldRespondWith`-          [json|[{"pId":"7","pName":"New Project","cId":"2"}]|]-          { matchStatus  = 201-          , matchHeaders = [ matchContentTypeJson-                           , "Location" <:> "/projects?id=eq.7"-                           , "Content-Range" <:> "*/*" ]-          }--      it "should not throw and return location header when selecting without PK" $-        request methodPost "/projects?select=name,client_id" [("Prefer", "return=representation")]-          [json|{"id":10,"name":"New Project","client_id":2}|] `shouldRespondWith`-          [json|[{"name":"New Project","client_id":2}]|]-          { matchStatus  = 201-          , matchHeaders = [ matchContentTypeJson-                           , "Location" <:> "/projects?id=eq.10"-                           , "Content-Range" <:> "*/*" ]-          }--    context "requesting headers only representation" $ do-      it "should not throw and return location header when selecting without PK" $-        request methodPost "/projects?select=name,client_id" [("Prefer", "return=headers-only")]-          [json|{"id":11,"name":"New Project","client_id":2}|] `shouldRespondWith` ""-          { matchStatus  = 201-          , matchHeaders = [ "Location" <:> "/projects?id=eq.11"-                           , "Content-Range" <:> "*/*" ]-          }--      when (actualPgVersion >= pgVersion110) $-        it "should not throw and return location header for partitioned tables when selecting without PK" $-          request methodPost "/car_models" [("Prefer", "return=headers-only")]-            [json|{"name":"Enzo","year":2021}|] `shouldRespondWith` ""-            { matchStatus  = 201-            , matchHeaders = [ "Location" <:> "/car_models?name=eq.Enzo&year=eq.2021"-                             , "Content-Range" <:> "*/*" ]-            }--    context "requesting no representation" $-      it "should not throw and return no location header when selecting without PK" $-        request methodPost "/projects?select=name,client_id" []-          [json|{"id":12,"name":"New Project","client_id":2}|] `shouldRespondWith` ""-          { matchStatus  = 201-          , matchHeaders = [ matchHeaderAbsent hLocation ]-          }--    context "from an html form" $-      it "accepts disparate json types" $ do-        request methodPost "/menagerie"-            [("Content-Type", "application/x-www-form-urlencoded")]-             ("integer=7&double=2.71828&varchar=forms+are+fun&" <>-              "boolean=false&date=1900-01-01&money=$3.99&enum=foo")-          `shouldRespondWith`-            ""-            { matchStatus = 201 }--    context "with no pk supplied" $ do-      context "into a table with auto-incrementing pk" $-        it "succeeds with 201 and location header" $ do-          -- reset pk sequence first to make test repeatable-          request methodPost "/rpc/reset_sequence"-              [("Prefer", "tx=commit")]-              [json|{"name": "auto_incrementing_pk_id_seq", "value": 2}|]-            `shouldRespondWith`-              [json|""|]--          request methodPost "/auto_incrementing_pk" [("Prefer", "return=headers-only")]-              [json| { "non_nullable_string":"not null"} |]-            `shouldRespondWith`-              ""-              { matchStatus  = 201-              , matchHeaders = [ "Location" <:> "/auto_incrementing_pk?id=eq.2" ]-              }--      context "into a table with simple pk" $-        it "fails with 400 and error" $-          post "/simple_pk" [json| { "extra":"foo"} |]-          `shouldRespondWith`-          (if actualPgVersion >= pgVersion130 then-            [json|{"hint":null,"details":"Failing row contains (null, foo).","code":"23502","message":"null value in column \"k\" of relation \"simple_pk\" violates not-null constraint"}|]-           else-            [json|{"hint":null,"details":"Failing row contains (null, foo).","code":"23502","message":"null value in column \"k\" violates not-null constraint"}|]-          )-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson]-          }--      context "into a table with no pk" $ do-        it "succeeds with 201 but no location header" $ do-          post "/no_pk" [json| { "a":"foo", "b":"bar" } |]-            `shouldRespondWith`-              ""-              { matchStatus  = 201-              , matchHeaders = [matchHeaderAbsent hLocation]-              }--        it "returns full details of inserted record if asked" $ do-          request methodPost "/no_pk"-              [("Prefer", "return=representation")]-              [json| { "a":"bar", "b":"baz" } |]-            `shouldRespondWith`-              [json| [{ "a":"bar", "b":"baz" }] |]-              { matchStatus  = 201-              , matchHeaders = [matchHeaderAbsent hLocation]-              }--        it "returns empty array when no items inserted, and return=rep" $ do-          request methodPost "/no_pk"-              [("Prefer", "return=representation")]-              [json| [] |]-            `shouldRespondWith`-              [json| [] |]-              { matchStatus = 201 }--        it "can post nulls" $ do-          request methodPost "/no_pk"-              [("Prefer", "return=representation")]-              [json| { "a":null, "b":"foo" } |]-            `shouldRespondWith`-              [json| [{ "a":null, "b":"foo" }] |]-              { matchStatus  = 201-              , matchHeaders = [matchHeaderAbsent hLocation]-              }--    context "with compound pk supplied" $-      it "builds response location header appropriately" $ do-        request methodPost "/compound_pk"-            [("Prefer", "return=representation")]-            [json| { "k1":12, "k2":"Rock & R+ll" } |]-          `shouldRespondWith`-            [json|[ { "k1":12, "k2":"Rock & R+ll", "extra": null } ]|]-            { matchStatus  = 201-            , matchHeaders = [ "Location" <:> "/compound_pk?k1=eq.12&k2=eq.Rock%20%26%20R%2Bll" ]-            }--    context "with bulk insert" $-      it "returns 201 but no location header" $ do-        let bulkData = [json| [ {"k1":21, "k2":"hello world"}-                              , {"k1":22, "k2":"bye for now"}]-                            |]-        request methodPost "/compound_pk" [] bulkData-          `shouldRespondWith`-            ""-            { matchStatus  = 201-            , matchHeaders = [matchHeaderAbsent hLocation]-            }--    context "with invalid json payload" $-      it "fails with 400 and error" $-        post "/simple_pk" "}{ x = 2"-        `shouldRespondWith`-        [json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]-        { matchStatus  = 400-        , matchHeaders = [matchContentTypeJson]-        }--    context "with no payload" $-      it "fails with 400 and error" $-        post "/simple_pk" ""-        `shouldRespondWith`-        [json|{"message":"Error in $: not enough input"}|]-        { matchStatus  = 400-        , matchHeaders = [matchContentTypeJson]-        }--    context "with valid json payload" $-      it "succeeds and returns 201 created" $-        post "/simple_pk" [json| { "k":"k1", "extra":"e1" } |] `shouldRespondWith` 201--    context "attempting to insert a row with the same primary key" $-      it "fails returning a 409 Conflict" $-        post "/simple_pk"-            [json| { "k":"xyyx", "extra":"e1" } |]-          `shouldRespondWith`-            [json|{"hint":null,"details":"Key (k)=(xyyx) already exists.","code":"23505","message":"duplicate key value violates unique constraint \"simple_pk_pkey\""}|]-            { matchStatus  = 409 }--    context "attempting to insert a row with conflicting unique constraint" $-      it "fails returning a 409 Conflict" $-        post "/withUnique"  [json| { "uni":"nodup", "extra":"e2" } |] `shouldRespondWith` 409--    context "jsonb" $ do-      it "serializes nested object" $ do-        let inserted = [json| { "data": { "foo":"bar" } } |]-        request methodPost "/json_table"-                     [("Prefer", "return=representation")]-                     inserted-          `shouldRespondWith` [json|[{"data":{"foo":"bar"}}]|]-          { matchStatus  = 201-          }--      it "serializes nested array" $ do-        let inserted = [json| { "data": [1,2,3] } |]-        request methodPost "/json_table"-                     [("Prefer", "return=representation")]-                     inserted-          `shouldRespondWith` [json|[{"data":[1,2,3]}]|]-          { matchStatus  = 201-          }--    context "empty objects" $ do-      it "successfully inserts a row with all-default columns" $ do-        post "/items" "{}" `shouldRespondWith` ""-          { matchStatus  = 201-          , matchHeaders = []-          }-        post "/items" "[{}]" `shouldRespondWith` ""-          { matchStatus  = 201-          , matchHeaders = []-          }--      it "successfully inserts two rows with all-default columns" $-        post "/items" "[{}, {}]" `shouldRespondWith` ""-          { matchStatus  = 201-          , matchHeaders = []-          }--      it "successfully inserts a row with all-default columns with prefer=rep" $ do-        -- reset pk sequence first to make test repeatable-        request methodPost "/rpc/reset_sequence"-            [("Prefer", "tx=commit")]-            [json|{"name": "items2_id_seq", "value": 20}|]-          `shouldRespondWith`-            [json|""|]--        request methodPost "/items2"-            [("Prefer", "return=representation")]-            [json|{}|]-          `shouldRespondWith`-            [json|[{ id: 20 }]|]-            { matchStatus  = 201 }--      it "successfully inserts a row with all-default columns with prefer=rep and &select=" $ do-        -- reset pk sequence first to make test repeatable-        request methodPost "/rpc/reset_sequence"-            [("Prefer", "tx=commit")]-            [json|{"name": "items3_id_seq", "value": 20}|]-          `shouldRespondWith`-            [json|""|]--        request methodPost "/items3?select=id"-            [("Prefer", "return=representation")]-            [json|{}|]-          `shouldRespondWith` [json|[{ id: 20 }]|]-            { matchStatus  = 201 }--    context "POST with ?columns parameter" $ do-      it "ignores json keys not included in ?columns" $ do-        request methodPost "/articles?columns=id,body" [("Prefer", "return=representation")]-          [json| {"id": 200, "body": "xxx", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith`-          [json|[{"id": 200, "body": "xxx", "owner": "postgrest_test_anonymous"}]|]-          { matchStatus  = 201-          , matchHeaders = [] }-        request methodPost "/articles?columns=id,body&select=id,body" [("Prefer", "return=representation")]-          [json| [-            {"id": 201, "body": "yyy", "smth": "here", "other": "stuff", "fake_id": 13},-            {"id": 202, "body": "zzz", "garbage": "%%$&", "kkk": "jjj"},-            {"id": 203, "body": "aaa", "hey": "ho"} ]|] `shouldRespondWith`-          [json|[-            {"id": 201, "body": "yyy"},-            {"id": 202, "body": "zzz"},-            {"id": 203, "body": "aaa"} ]|]-          { matchStatus  = 201-          , matchHeaders = [] }--      -- TODO parse columns error message needs to be improved-      it "disallows blank ?columns" $-        post "/articles?columns="-          [json|[-            {"id": 204, "body": "yyy"},-            {"id": 205, "body": "zzz"}]|]-          `shouldRespondWith`-          [json|  {"details":"unexpected end of input expecting field name (* or [a..z0..9_])","message":"\"failed to parse columns parameter ()\" (line 1, column 1)"} |]-          { matchStatus  = 400-          , matchHeaders = []-          }--      it "disallows array elements that are not json objects" $-        post "/articles?columns=id,body"-          [json|[-            {"id": 204, "body": "yyy"},-            333,-            "asdf",-            {"id": 205, "body": "zzz"}]|] `shouldRespondWith`-          [json|{-              "code": "22023",-              "details": null,-              "hint": null,-              "message": "argument of json_populate_recordset must be an array of objects"}|]-          { matchStatus  = 400-          , matchHeaders = []-          }--  describe "CSV insert" $ do-    context "disparate csv types" $-      it "succeeds with multipart response" $ do-        pendingWith "Decide on what to do with CSV insert"-        let inserted = [str|integer,double,varchar,boolean,date,money,enum-            |13,3.14159,testing!,false,1900-01-01,$3.99,foo-            |12,0.1,a string,true,1929-10-01,12,bar-            |]-        request methodPost "/menagerie" [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] inserted-           `shouldRespondWith` ResponseMatcher-           { matchStatus  = 201-           , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]-           , matchBody = bodyEquals inserted-           }--    context "requesting full representation" $ do-      it "returns full details of inserted record" $-        request methodPost "/no_pk"-                     [("Content-Type", "text/csv"), ("Accept", "text/csv"),  ("Prefer", "return=representation")]-                     "a,b\nbar,baz"-          `shouldRespondWith` "a,b\nbar,baz"-          { matchStatus  = 201-          , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]-          }--      it "can post nulls" $-        request methodPost "/no_pk"-                     [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]-                     "a,b\nNULL,foo"-          `shouldRespondWith` "a,b\n,foo"-          { matchStatus  = 201-          , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]-          }--      it "only returns the requested column header with its associated data" $-        request methodPost "/projects?select=id"-                     [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]-                     "id,name,client_id\n8,Xenix,1\n9,Windows NT,1"-          `shouldRespondWith` "id\n8\n9"-          { matchStatus  = 201-          , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",-                            "Content-Range" <:> "*/*"]-          }--    context "with wrong number of columns" $-      it "fails for too few" $-        request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz"-        `shouldRespondWith`-        [json|{"message":"All lines must have same number of fields"}|]-        { matchStatus  = 400-        , matchHeaders = [matchContentTypeJson]-        }--    context "with unicode values" $-      it "succeeds and returns usable location header" $ do-        p <- request methodPost "/simple_pk2?select=extra,k"-            [("Prefer", "tx=commit"), ("Prefer", "return=representation")]-            [json| { "k":"圍棋", "extra":"¥" } |]-        pure p `shouldRespondWith`-          [json|[ { "k":"圍棋", "extra":"¥" } ]|]-          { matchStatus = 201 }--        let Just location = lookup hLocation $ simpleHeaders p-        get location-          `shouldRespondWith`-            [json|[ { "k":"圍棋", "extra":"¥" } ]|]--        request methodDelete location-            [("Prefer", "tx=commit")]-            ""-          `shouldRespondWith`-            204--  describe "Row level permission" $-    it "set user_id when inserting rows" $ do-      request methodPost "/authors_only"-          [ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0", ("Prefer", "return=representation") ]-          [json| { "secret": "nyancat" } |]-        `shouldRespondWith`-          [json|[{"owner":"jdoe","secret":"nyancat"}]|]-          { matchStatus  = 201 }--      request methodPost "/authors_only"-          -- jwt token for jroe-          [ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.2e7mx0U4uDcInlbJVOBGlrRufwqWLINDIEDC1vS0nw8", ("Prefer", "return=representation") ]-          [json| { "secret": "lolcat", "owner": "hacker" } |]-        `shouldRespondWith`-          [json|[{"owner":"jroe","secret":"lolcat"}]|]-          { matchStatus  = 201 }--  context "tables with self reference foreign keys" $ do-    it "embeds parent after insert" $-      request methodPost "/web_content?select=id,name,parent_content:p_web_id(name)"-              [("Prefer", "return=representation")]-        [json|{"id":6, "name":"wot", "p_web_id":4}|]-        `shouldRespondWith`-        [json|[{"id":6,"name":"wot","parent_content":{"name":"wut"}}]|]-        { matchStatus  = 201-        , matchHeaders = [ matchContentTypeJson , "Location" <:> "/web_content?id=eq.6" ]-        }--  context "table with limited privileges" $ do-    it "succeeds inserting if correct select is applied" $-      request methodPost "/limited_article_stars?select=article_id,user_id" [("Prefer", "return=representation")]-        [json| {"article_id": 2, "user_id": 1} |] `shouldRespondWith` [json|[{"article_id":2,"user_id":1}]|]-        { matchStatus  = 201-        , matchHeaders = []-        }--    it "fails inserting if more columns are selected" $-      request methodPost "/limited_article_stars?select=article_id,user_id,created_at" [("Prefer", "return=representation")]-        [json| {"article_id": 2, "user_id": 2} |] `shouldRespondWith` (-      if actualPgVersion >= pgVersion112 then-      [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]-         else-      [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]-                                                                      )-        { matchStatus  = 401-        , matchHeaders = []-        }--    it "fails inserting if select is not specified" $-      request methodPost "/limited_article_stars" [("Prefer", "return=representation")]-        [json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` (-      if actualPgVersion >= pgVersion112 then-      [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]-         else-      [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]-                                                                      )-        { matchStatus  = 401-        , matchHeaders = []-        }--    it "can insert in a table with no select and return=minimal" $ do-      request methodPost "/insertonly"-          [("Prefer", "return=minimal")]-          [json| { "v":"some value" } |]-        `shouldRespondWith`-          ""-          { matchStatus = 201 }--  describe "Inserting into VIEWs" $ do-    context "requesting no representation" $-      it "succeeds with 201" $-        post "/compound_pk_view"-            [json|{"k1":1,"k2":"test","extra":2}|]-          `shouldRespondWith`-            ""-            { matchStatus  = 201-            , matchHeaders = [ matchHeaderAbsent hLocation ]-            }--    context "requesting header only representation" $ do-      it "returns a location header" $-        request methodPost "/compound_pk_view" [("Prefer", "return=headers-only")]-            [json|{"k1":1,"k2":"test","extra":2}|]-          `shouldRespondWith`-            ""-            { matchStatus  = 201-            , matchHeaders = [ "Location" <:> "/compound_pk_view?k1=eq.1&k2=eq.test"-                             , "Content-Range" <:> "*/*" ]-            }--      it "should not throw and return location header when a PK is null" $-        request methodPost "/test_null_pk_competitors_sponsors" [("Prefer", "return=headers-only")]-            [json|{"id":1}|]-          `shouldRespondWith`-            ""-            { matchStatus  = 201-            , matchHeaders = [ "Location" <:> "/test_null_pk_competitors_sponsors?id=eq.1&sponsor_id=is.null"-                             , "Content-Range" <:> "*/*" ]-            }
− test/Feature/JsonOperatorSpec.hs
@@ -1,253 +0,0 @@-module Feature.JsonOperatorSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import PostgREST.Config.PgVersion (PgVersion, pgVersion112,-                                   pgVersion121)--import Protolude  hiding (get)-import SpecHelper--spec :: PgVersion -> SpecWith ((), Application)-spec actualPgVersion = 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` (-        if actualPgVersion >= pgVersion121 then-        [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for type integer: \"baz\""} |]-        else-        [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]-                            )-        { matchStatus  = 400 , matchHeaders = [] }--    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` (-        if actualPgVersion >= pgVersion112 then-        [json|-          {"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.",-           "details":null,"code":"42883","message":"operator does not exist: text -> integer"} |]-           else-        [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` (-        if actualPgVersion >= pgVersion112 then-        [json|-          {"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.",-           "details":null,"code":"42883","message":"operator does not exist: text -> unknown"} |]-           else-        [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_table?data->foo->>bar=eq.baz" `shouldRespondWith`-        [json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]-        { matchHeaders = [matchContentTypeJson] }-      get "/json_table?data->foo->>bar=eq.fake" `shouldRespondWith`-        [json| [] |]-        { matchHeaders = [matchContentTypeJson] }--    it "can filter by properties inside json column using not" $-      get "/json_table?data->foo->>bar=not.eq.baz" `shouldRespondWith`-        [json| [] |]-        { matchHeaders = [matchContentTypeJson] }--    it "can filter by properties inside json column using ->>" $-      get "/json_table?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_table?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_table?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-      request methodPatch "/json_table?data->>id=eq.3"-          [("Prefer", "return=representation")]-          [json| { "data": { "id":" \"escaped" } } |]-        `shouldRespondWith`-          [json| [{ "data": { "id":" \"escaped" } }] |]--  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] }
− test/Feature/LegacyGucsSpec.hs
@@ -1,68 +0,0 @@-module Feature.LegacyGucsSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec          hiding (pendingWith)-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude  hiding (get)-import SpecHelper--spec :: SpecWith ((), Application)-spec =-  describe "remote procedure call with legacy gucs disabled" $ do-    it "custom header is set" $-      request methodPost "/rpc/get_guc_value" [("Custom-Header", "test")]-        [json| { "prefix": "request.headers", "name": "custom-header" } |]-          `shouldRespondWith`-          [json|"test"|]-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson ]-          }--    it "standard header is set" $-      request methodPost "/rpc/get_guc_value" [("Origin", "http://example.com")]-        [json| { "prefix": "request.headers", "name": "origin" } |]-          `shouldRespondWith`-          [json|"http://example.com"|]-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson ]-          }--    it "current role is available as GUC claim" $-      request methodPost "/rpc/get_guc_value" []-        [json| { "prefix": "request.jwt.claims", "name": "role" } |]-          `shouldRespondWith`-          [json|"postgrest_test_anonymous"|]-          { matchStatus  = 200-          , matchHeaders = [ matchContentTypeJson ]-          }--    it "single cookie ends up as claims" $-      request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]-        [json| {"prefix": "request.cookies", "name":"acookie"} |]-          `shouldRespondWith`-          [json|"cookievalue"|]-          { matchStatus = 200-          , matchHeaders = []-          }--    it "multiple cookies ends up as claims" $-      request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]-        [json| {"prefix": "request.cookies", "name":"secondcookie"} |]-          `shouldRespondWith`-          [json|"anothervalue"|]-          { matchStatus = 200-          , matchHeaders = []-          }--    it "gets the Authorization value" $-      request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]-        [json| {"prefix": "request.headers", "name":"authorization"} |]-          `shouldRespondWith`-          [json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]-          { matchStatus = 200-          , matchHeaders = []-          }
− test/Feature/MultipleSchemaSpec.hs
@@ -1,331 +0,0 @@-module Feature.MultipleSchemaSpec where--import Control.Lens    ((^?))-import Data.Aeson.Lens-import Data.Aeson.QQ--import Network.HTTP.Types-import Network.Wai        (Application)-import Network.Wai.Test   (SResponse (simpleHeaders), simpleBody)--import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude-import SpecHelper--spec :: SpecWith ((), Application)-spec =-  describe "multiple schemas in single instance" $ do-    context "Reading tables on different schemas" $ do-      it "succeeds in reading table from default schema v1 if no schema is selected via header" $-        request methodGet "/parents" [] "" `shouldRespondWith`-          [json|[-            {"id":1,"name":"parent v1-1"},-            {"id":2,"name":"parent v1-2"}-          ]|]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]-          }--      it "succeeds in reading table from default schema v1 after explicitly passing it in the header" $-        request methodGet "/parents" [("Accept-Profile", "v1")] "" `shouldRespondWith`-          [json|[-            {"id":1,"name":"parent v1-1"},-            {"id":2,"name":"parent v1-2"}-          ]|]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]-          }--      it "succeeds in reading table from schema v2" $-        request methodGet "/parents" [("Accept-Profile", "v2")] "" `shouldRespondWith`-          [json|[-            {"id":3,"name":"parent v2-3"},-            {"id":4,"name":"parent v2-4"}-          ]|]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]-          }--      it "succeeds in reading another_table from schema v2" $-        request methodGet "/another_table" [("Accept-Profile", "v2")] "" `shouldRespondWith`-          [json|[-            {"id":5,"another_value":"value 5"},-            {"id":6,"another_value":"value 6"}-          ]|]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]-          }--      it "doesn't find another_table in schema v1" $-        request methodGet "/another_table" [("Accept-Profile", "v1")] "" `shouldRespondWith` 404--      it "fails trying to read table from unkown schema" $-        request methodGet "/parents" [("Accept-Profile", "unkown")] "" `shouldRespondWith`-          [json|{"message":"The schema must be one of the following: v1, v2"}|]-          {-            matchStatus = 406-          }--    context "Inserting tables on different schemas" $ do-      it "succeeds inserting on default schema and returning it" $-        request methodPost "/children"-            [("Prefer", "return=representation")]-            [json|{"id": 0, "name": "child v1-1", "parent_id": 1}|]-          `shouldRespondWith`-            [json|[{"id": 0, "name": "child v1-1", "parent_id": 1}]|]-            {-              matchStatus = 201-            , matchHeaders = ["Content-Profile" <:> "v1"]-            }--      it "succeeds inserting on the v1 schema and returning its parent" $-        request methodPost "/children?select=id,parent(*)"-            [("Prefer", "return=representation"), ("Content-Profile", "v1")]-            [json|{"id": 0, "name": "child v1-2", "parent_id": 2}|]-          `shouldRespondWith`-            [json|[{"id": 0, "parent": {"id": 2, "name": "parent v1-2"}}]|]-            {-              matchStatus = 201-            , matchHeaders = ["Content-Profile" <:> "v1"]-            }--      it "succeeds inserting on the v2 schema and returning its parent" $-        request methodPost "/children?select=id,parent(*)"-            [("Prefer", "return=representation"), ("Content-Profile", "v2")]-            [json|{"id": 0, "name": "child v2-3", "parent_id": 3}|]-          `shouldRespondWith`-            [json|[{"id": 0, "parent": {"id": 3, "name": "parent v2-3"}}]|]-            {-              matchStatus = 201-            , matchHeaders = ["Content-Profile" <:> "v2"]-            }--      it "fails when inserting on an unknown schema" $-        request methodPost "/children" [("Content-Profile", "unknown")]-          [json|{"name": "child 4", "parent_id": 4}|]-          `shouldRespondWith`-          [json|{"message":"The schema must be one of the following: v1, v2"}|]-          {-            matchStatus = 406-          }--    context "calling procs on different schemas" $ do-      it "succeeds in calling the default schema proc" $-        request methodGet "/rpc/get_parents_below?id=6" [] ""-          `shouldRespondWith`-          [json|[{"id":1,"name":"parent v1-1"}, {"id":2,"name":"parent v1-2"}]|]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]-          }--      it "succeeds in calling the v1 schema proc and embedding" $-        request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v1")] ""-          `shouldRespondWith`-          [json| [-            {"id":1,"name":"parent v1-1","children":[{"id":1,"name":"child v1-1"}]},-            {"id":2,"name":"parent v1-2","children":[{"id":2,"name":"child v1-2"}]}] |]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]-          }--      it "succeeds in calling the v2 schema proc and embedding" $-        request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v2")] ""-          `shouldRespondWith`-          [json| [-            {"id":3,"name":"parent v2-3","children":[{"id":1,"name":"child v2-3"}]},-            {"id":4,"name":"parent v2-4","children":[]}] |]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]-          }--      it "succeeds in calling the v2 schema proc with POST by using Content-Profile" $-        request methodPost "/rpc/get_parents_below?select=id,name" [("Content-Profile", "v2")]-          [json|{"id": "6"}|]-          `shouldRespondWith`-          [json| [-            {"id":3,"name":"parent v2-3"},-            {"id":4,"name":"parent v2-4"}]|]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]-          }--    context "Modifying tables on different schemas" $ do-      it "succeeds in patching on the v1 schema and returning its parent" $-        request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v1"), ("Prefer", "return=representation")]-          [json|{"name": "child v1-1 updated"}|]-          `shouldRespondWith`-          [json|[{"name":"child v1-1 updated", "parent": {"name": "parent v1-1"}}]|]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]-          }--      it "succeeds in patching on the v2 schema and returning its parent" $-        request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")]-          [json|{"name": "child v2-1 updated"}|]-          `shouldRespondWith`-          [json|[{"name":"child v2-1 updated", "parent": {"name": "parent v2-3"}}]|]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]-          }--      it "succeeds on deleting on the v2 schema" $ do-        request methodDelete "/children?id=eq.1"-            [("Content-Profile", "v2"), ("Prefer", "return=representation")]-            ""-          `shouldRespondWith`-            [json|[{"id": 1, "name": "child v2-3", "parent_id": 3}]|]-            { matchHeaders = ["Content-Profile" <:> "v2"] }--      it "succeeds on PUT on the v2 schema" $-        request methodPut "/children?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]-          [json| [ { "id": 111, "name": "child v2-111", "parent_id": null } ]|]-          `shouldRespondWith`-          [json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|]-          {-            matchStatus = 200-          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]-          }--    context "OpenAPI output" $ do-      it "succeeds in reading table definition from default schema v1 if no schema is selected via header" $ do-          req <- request methodGet "/" [] ""--          liftIO $ do-            simpleHeaders req `shouldSatisfy` matchHeader "Content-Profile" "v1"--            let def = simpleBody req ^? key "definitions" . key "parents"--            def `shouldBe` Just-                [aesonQQ|-                  {-                    "type" : "object",-                    "properties" : {-                      "id" : {-                        "description" : "Note:\nThis is a Primary Key.<pk/>",-                        "format" : "integer",-                        "type" : "integer"-                      },-                      "name" : {-                        "format" : "text",-                        "type" : "string"-                      }-                    },-                    "required" : [-                      "id"-                    ]-                  }-                |]--      it "succeeds in reading table definition from default schema v1 after explicitly passing it in the header" $ do-          r <- request methodGet "/" [("Accept-Profile", "v1")] ""--          liftIO $ do-            simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v1"--            let def = simpleBody r ^? key "definitions" . key "parents"--            def `shouldBe` Just-                [aesonQQ|-                  {-                    "type" : "object",-                    "properties" : {-                      "id" : {-                        "description" : "Note:\nThis is a Primary Key.<pk/>",-                        "format" : "integer",-                        "type" : "integer"-                      },-                      "name" : {-                        "format" : "text",-                        "type" : "string"-                      }-                    },-                    "required" : [-                      "id"-                    ]-                  }-                |]--      it "succeeds in reading table definition from schema v2" $ do-          r <- request methodGet "/" [("Accept-Profile", "v2")] ""--          liftIO $ do-            simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v2"--            let def = simpleBody r ^? key "definitions" . key "parents"--            def `shouldBe` Just-                [aesonQQ|-                  {-                    "type" : "object",-                    "properties" : {-                      "id" : {-                        "description" : "Note:\nThis is a Primary Key.<pk/>",-                        "format" : "integer",-                        "type" : "integer"-                      },-                      "name" : {-                        "format" : "text",-                        "type" : "string"-                      }-                    },-                    "required" : [-                      "id"-                    ]-                  }-                |]--      it "succeeds in reading another_table definition from schema v2" $ do-          r <- request methodGet "/" [("Accept-Profile", "v2")] ""--          liftIO $ do-            simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v2"--            let def = simpleBody r ^? key "definitions" . key "another_table"--            def `shouldBe` Just-                [aesonQQ|-                  {-                    "type" : "object",-                    "properties" : {-                      "id" : {-                        "description" : "Note:\nThis is a Primary Key.<pk/>",-                        "format" : "integer",-                        "type" : "integer"-                      },-                      "another_value" : {-                        "format" : "text",-                        "type" : "string"-                      }-                    },-                    "required" : [-                      "id"-                    ]-                  }-                |]--      it "doesn't find another_table definition in schema v1" $ do-        r <- request methodGet "/" [("Accept-Profile", "v1")] ""--        liftIO $ do-          let def = simpleBody r ^? key "definitions" . key "another_table"-          def `shouldBe` Nothing--      it "fails trying to read definitions from unkown schema" $-        request methodGet "/" [("Accept-Profile", "unkown")] "" `shouldRespondWith`-          [json|{"message":"The schema must be one of the following: v1, v2"}|]-          {-            matchStatus = 406-          }
− test/Feature/NoJwtSpec.hs
@@ -1,31 +0,0 @@-module Feature.NoJwtSpec where---- {{{ Imports--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude-import SpecHelper--- }}}--spec :: SpecWith ((), Application)-spec = describe "server started without JWT secret" $ do--  -- this test will stop working 9999999999s after the UNIX EPOCH-  it "responds with error on attempted auth" $ do-    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"-    request methodGet "/authors_only" [auth] ""-      `shouldRespondWith`-      [json|{"message":"Server lacks JWT secret"}|]-      { matchStatus  = 500-      , matchHeaders = [ matchContentTypeJson ]-      }--  it "behaves normally when user does not attempt auth" $-    request methodGet "/items" [] ""-      `shouldRespondWith` 200
− test/Feature/NonexistentSchemaSpec.hs
@@ -1,17 +0,0 @@-module Feature.NonexistentSchemaSpec where--import Network.Wai (Application)--import Test.Hspec-import Test.Hspec.Wai--import Protolude hiding (get)--spec :: SpecWith ((), Application)-spec =-  describe "Non existent api schema" $ do-    it "succeeds when requesting root path" $-      get "/" `shouldRespondWith` 200--    it "gives 404 when requesting a nonexistent table in this nonexistent schema" $-      get "/nonexistent_table" `shouldRespondWith` 404
− test/Feature/OpenApiSpec.hs
@@ -1,589 +0,0 @@-module Feature.OpenApiSpec where--import Control.Lens     ((^?))-import Data.Aeson.Types (Value (..))-import Network.Wai      (Application)-import Network.Wai.Test (SResponse (..))--import Data.Aeson.Lens-import Data.Aeson.QQ-import Network.HTTP.Types-import Test.Hspec         hiding (pendingWith)-import Test.Hspec.Wai--import PostgREST.Config.PgVersion (PgVersion, pgVersion100,-                                   pgVersion110)--import PostgREST.Version (docsVersion)-import Protolude         hiding (get)-import SpecHelper--spec :: PgVersion -> SpecWith ((), Application)-spec actualPgVersion = describe "OpenAPI" $ do-  it "root path returns a valid openapi spec" $ do-    validateOpenApiResponse [("Accept", "application/openapi+json")]-    request methodHead "/" (acceptHdrs "application/openapi+json") ""-      `shouldRespondWith` "" { matchStatus  = 200 }--  it "should respond to openapi request on none root path with 415" $-    request methodGet "/items"-            (acceptHdrs "application/openapi+json") ""-      `shouldRespondWith` 415--  it "includes postgrest.org current version api docs" $ do-    r <- simpleBody <$> get "/"--    let docsUrl = r ^? key "externalDocs" . key "url"--    liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))--  describe "table" $ do--    it "includes paths to tables" $ do-      r <- simpleBody <$> get "/"--      let method s = key "paths" . key "/child_entities" . key s-          childGetSummary = r ^? method "get" . key "summary"-          childGetDescription = r ^? method "get" . key "description"-          getParameters = r ^? method "get" . key "parameters"-          postParameters = r ^? method "post" . key "parameters"-          postResponse = r ^? method "post" . key "responses" . key "201" . key "description"-          patchResponse = r ^? method "patch" . key "responses" . key "204" . key "description"-          deleteResponse = r ^? method "delete" . key "responses" . key "204" . key "description"--      let grandChildGet s = key "paths" . key "/grandchild_entities" . key "get" . key s-          grandChildGetSummary = r ^? grandChildGet "summary"-          grandChildGetDescription = r ^? grandChildGet "description"--      liftIO $ do--        childGetSummary `shouldBe` Just "child_entities comment"--        childGetDescription `shouldBe` Nothing--        grandChildGetSummary `shouldBe` Just "grandchild_entities summary"--        grandChildGetDescription `shouldBe` Just "grandchild_entities description\nthat spans\nmultiple lines"--        getParameters `shouldBe` Just-          [aesonQQ|-            [-              { "$ref": "#/parameters/rowFilter.child_entities.id" },-              { "$ref": "#/parameters/rowFilter.child_entities.name" },-              { "$ref": "#/parameters/rowFilter.child_entities.parent_id" },-              { "$ref": "#/parameters/select" },-              { "$ref": "#/parameters/order" },-              { "$ref": "#/parameters/range" },-              { "$ref": "#/parameters/rangeUnit" },-              { "$ref": "#/parameters/offset" },-              { "$ref": "#/parameters/limit" },-              { "$ref": "#/parameters/preferCount" }-            ]-          |]--        postParameters `shouldBe` Just-          [aesonQQ|-            [-              { "$ref": "#/parameters/body.child_entities" },-              { "$ref": "#/parameters/select" },-              { "$ref": "#/parameters/preferReturn" }-            ]-          |]--        postResponse `shouldBe` Just "Created"--        patchResponse `shouldBe` Just "No Content"--        deleteResponse `shouldBe` Just "No Content"--    it "includes an array type for GET responses" $ do-      r <- simpleBody <$> get "/"--      let childGetSchema = r ^? key "paths"-            . key "/child_entities"-            . key "get"-            . key "responses"-            . key "200"-            . key "schema"--      liftIO $-        childGetSchema `shouldBe` Just-          [aesonQQ|-            {-              "items": {-                "$ref": "#/definitions/child_entities"-              },-              "type": "array"-            }-          |]--    it "includes definitions to tables" $ do-      r <- simpleBody <$> get "/"--      let def = r ^? key "definitions" . key "child_entities"--      liftIO $--        def `shouldBe` Just-          [aesonQQ|-            {-              "type": "object",-              "description": "child_entities comment",-              "properties": {-                "id": {-                  "description": "child_entities id comment\n\nNote:\nThis is a Primary Key.<pk/>",-                  "format": "integer",-                  "type": "integer"-                },-                "name": {-                  "description": "child_entities name comment. Can be longer than sixty-three characters long",-                  "format": "text",-                  "type": "string"-                },-                "parent_id": {-                  "description": "Note:\nThis is a Foreign Key to `entities.id`.<fk table='entities' column='id'/>",-                  "format": "integer",-                  "type": "integer"-                }-              },-              "required": [-                "id"-              ]-            }-          |]--    it "doesn't include privileged table for anonymous" $ do-      r <- simpleBody <$> get "/"-      let tablePath = r ^? key "paths" . key "/authors_only"--      liftIO $ tablePath `shouldBe` Nothing--    it "includes table if user has permission" $ do-      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"-      r <- simpleBody <$> request methodGet "/" [auth] ""-      let tableTag = r ^? key "paths" . key "/authors_only"-                    . key "post"  . key "tags"-                    . nth 0-      liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|]--  describe "Foreign table" $--    it "includes foreign table properties" $ do-      r <- simpleBody <$> get "/"--      let method s = key "paths" . key "/projects_dump" . key s-          getSummary = r ^? method "get" . key "summary"-          getDescription = r ^? method "get" . key "description"-          getParameters = r ^? method "get" . key "parameters"--      liftIO $ do--        getSummary `shouldBe` Just "A temporary projects dump"--        getDescription `shouldBe` Just "Just a test for foreign tables"--        getParameters `shouldBe` Just-          [aesonQQ|-            [-              { "$ref": "#/parameters/rowFilter.projects_dump.id" },-              { "$ref": "#/parameters/rowFilter.projects_dump.name" },-              { "$ref": "#/parameters/rowFilter.projects_dump.client_id" },-              { "$ref": "#/parameters/select" },-              { "$ref": "#/parameters/order" },-              { "$ref": "#/parameters/range" },-              { "$ref": "#/parameters/rangeUnit" },-              { "$ref": "#/parameters/offset" },-              { "$ref": "#/parameters/limit" },-              { "$ref": "#/parameters/preferCount" }-            ]-          |]--  when (actualPgVersion >= pgVersion100) $ do-    describe "Partitioned table" $--      it "includes partitioned table properties" $ do-        r <- simpleBody <$> get "/"--        let method s = key "paths" . key "/car_models" . key s-            getSummary = r ^? method "get" . key "summary"-            getDescription = r ^? method "get" . key "description"-            getParameterName = r ^? method "get" . key "parameters" . nth 0 . key "$ref"-            getParameterYear = r ^? method "get" . key "parameters" . nth 1 . key "$ref"-            getParameterRef = r ^? method "get" . key "parameters" . nth 2 . key "$ref"--        liftIO $ do--          getSummary `shouldBe` Just "A partitioned table"--          getDescription `shouldBe` Just "A test for partitioned tables"--          getParameterName `shouldBe` Just "#/parameters/rowFilter.car_models.name"--          getParameterYear `shouldBe` Just "#/parameters/rowFilter.car_models.year"--          when (actualPgVersion >= pgVersion110) $-            getParameterRef `shouldBe` Just "#/parameters/rowFilter.car_models.car_brand_name"--  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 "VIEW that has a source FK based on a UNIQUE key" $--    it "includes fk description" $ do-      r <- simpleBody <$> get "/"--      let referralLink = r ^? key "definitions" . key "referrals" . key "properties" . key "link"--      liftIO $-        referralLink `shouldBe` Just-          [aesonQQ|-            {-              "format": "integer",-              "type": "integer",-              "description": "Note:\nThis is a Foreign Key to `pages.link`.<fk table='pages' column='link'/>"-            }-          |]--  describe "PostgreSQL to Swagger Type Mapping" $ do--    it "character varying to string" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_character_varying"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "format": "character varying",-              "type": "string"-            }-          |]-    it "character(1) to string" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_character"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "maxLength": 1,-              "format": "character",-              "type": "string"-            }-          |]--    it "text to string" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_text"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "format": "text",-              "type": "string"-            }-          |]--    it "boolean to boolean" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_boolean"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "format": "boolean",-              "type": "boolean"-            }-          |]--    it "smallint to integer" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_smallint"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "format": "smallint",-              "type": "integer"-            }-          |]--    it "integer to integer" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_integer"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "format": "integer",-              "type": "integer"-            }-          |]--    it "bigint to integer" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_bigint"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "format": "bigint",-              "type": "integer"-            }-          |]--    it "numeric to number" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_numeric"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "format": "numeric",-              "type": "number"-            }-          |]--    it "real to number" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_real"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "format": "real",-              "type": "number"-            }-          |]--    it "double_precision to number" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_double_precision"--      liftIO $--        types `shouldBe` Just-          [aesonQQ|-            {-              "format": "double precision",-              "type": "number"-            }-          |]--  describe "Detects default values" $ do--    it "text" $ do-      r <- simpleBody <$> get "/"--      let defaultValue = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "text" . key "default"--      liftIO $--        defaultValue `shouldBe` Just "default"--    it "boolean" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "boolean" . key "default"--      liftIO $--        types `shouldBe` Just (Bool False)--    it "integer" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "integer" . key "default"--      liftIO $--        types `shouldBe` Just (Number 42)--    it "numeric" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "numeric" . key "default"--      liftIO $--        types `shouldBe` Just (Number 42.2)--    it "date" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "date" . key "default"--      liftIO $--        types `shouldBe` Just "1900-01-01"--    it "time" $ do-      r <- simpleBody <$> get "/"--      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "time" . key "default"--      liftIO $--        types `shouldBe` Just "13:00:00"--  describe "RPC" $ do--    it "includes function summary/description and body schema for arguments" $ do-      r <- simpleBody <$> get "/"--      let method s = key "paths" . key "/rpc/varied_arguments" . key s-          args = r ^? method "post" . key "parameters" . nth 0 . key "schema"-          summary = r ^? method "post" . key "summary"-          description = r ^? method "post" . key "description"--      liftIO $ do--        summary `shouldBe` Just "An RPC function"--        description `shouldBe` Just "Just a test for RPC function arguments"--        args `shouldBe` Just-          [aesonQQ|-            {-              "required": [-                "double",-                "varchar",-                "boolean",-                "date",-                "money",-                "enum",-                "arr"-              ],-              "properties": {-                "double": {-                  "format": "double precision",-                  "type": "number"-                },-                "varchar": {-                  "format": "character varying",-                  "type": "string"-                },-                "boolean": {-                  "format": "boolean",-                  "type": "boolean"-                },-                "date": {-                  "format": "date",-                  "type": "string"-                },-                "money": {-                  "format": "money",-                  "type": "string"-                },-                "enum": {-                  "format": "enum_menagerie_type",-                  "type": "string"-                },-                "arr": {-                  "format": "text[]",-                  "type": "string"-                },-                "integer": {-                  "format": "integer",-                  "type": "integer"-                },-                "json": {-                  "format": "json",-                  "type": "string"-                },-                "jsonb": {-                  "format": "jsonb",-                  "type": "string"-                }-              },-              "type": "object",-              "description": "An RPC function\n\nJust a test for RPC function arguments"-            }-          |]--    it "doesn't include privileged function for anonymous" $ do-      r <- simpleBody <$> get "/"-      let funcPath = r ^? key "paths" . key "/rpc/privileged_hello"--      liftIO $ funcPath `shouldBe` Nothing--    it "includes function if user has permission" $ do-      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"-      r <- simpleBody <$> request methodGet "/" [auth] ""-      let funcTag = r ^? key "paths" . key "/rpc/privileged_hello"-                    . key "post"  . key "tags"-                    . nth 0--      liftIO $ funcTag `shouldBe` Just [aesonQQ|"(rpc) privileged_hello"|]--    it "doesn't include OUT params of function as required parameters" $ do-      r <- simpleBody <$> get "/"-      let params = r ^? key "paths" . key "/rpc/many_out_params"-                      . key "post" . key "parameters" .  nth 0-                      . key "schema". key "required"--      liftIO $ params `shouldBe` Nothing--    it "includes INOUT params(with no DEFAULT) of function as required parameters" $ do-      r <- simpleBody <$> get "/"-      let params = r ^? key "paths" . key "/rpc/many_inout_params"-                      . key "post" . key "parameters" .  nth 0-                      . key "schema". key "required"--      liftIO $ params `shouldBe` Just [aesonQQ|["num", "str"]|]-
− test/Feature/OptionsSpec.hs
@@ -1,87 +0,0 @@-module Feature.OptionsSpec where--import Network.Wai      (Application)-import Network.Wai.Test (SResponse (..))--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai--import PostgREST.Config.PgVersion (PgVersion, pgVersion100,-                                   pgVersion110)--import Protolude-import SpecHelper--spec :: PgVersion -> SpecWith ((), Application)-spec actualPgVersion = describe "Allow header" $ do-  context "a table" $ do-    it "includes read/write verbs for writeable table" $ do-      r <- request methodOptions "/items" [] ""-      liftIO $-        simpleHeaders r `shouldSatisfy`-          matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"--  when (actualPgVersion >= pgVersion100) $-    context "a partitioned table" $ do-      it "includes read/write verbs for writeable partitioned tables" $ do-        r <- request methodOptions "/car_models" [] ""-        liftIO $-          simpleHeaders r `shouldSatisfy`-            matchHeader "Allow" (-              if actualPgVersion >= pgVersion110 then-                "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"-              else-                "OPTIONS,GET,HEAD,POST,PATCH,DELETE"-            )--  context "a view" $ do-    context "auto updatable" $ do-      it "includes read/write verbs for auto updatable views with pk" $ do-        r <- request methodOptions "/projects_auto_updatable_view_with_pk" [] ""-        liftIO $-          simpleHeaders r `shouldSatisfy`-            matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"--      it "includes read/write verbs for auto updatable views without pk" $ do-        r <- request methodOptions "/projects_auto_updatable_view_without_pk" [] ""-        liftIO $-          simpleHeaders r `shouldSatisfy`-            matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PATCH,DELETE"--    context "non auto updatable" $ do-      it "includes read verbs for non auto updatable views" $ do-        r <- request methodOptions "/projects_view_without_triggers" [] ""-        liftIO $-          simpleHeaders r `shouldSatisfy`-            matchHeader "Allow" "OPTIONS,GET,HEAD"--      it "includes read/write verbs for insertable, updatable and deletable views with pk" $ do-        r <- request methodOptions "/projects_view_with_all_triggers_with_pk" [] ""-        liftIO $-          simpleHeaders r `shouldSatisfy`-            matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"--      it "includes read/write verbs for insertable, updatable and deletable views without pk" $ do-        r <- request methodOptions "/projects_view_with_all_triggers_without_pk" [] ""-        liftIO $-          simpleHeaders r `shouldSatisfy`-            matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PATCH,DELETE"--      it "includes read and insert verbs for insertable views" $ do-        r <- request methodOptions "/projects_view_with_insert_trigger" [] ""-        liftIO $-          simpleHeaders r `shouldSatisfy`-            matchHeader "Allow" "OPTIONS,GET,HEAD,POST"--      it "includes read and update verbs for updatable views" $ do-        r <- request methodOptions "/projects_view_with_update_trigger" [] ""-        liftIO $-          simpleHeaders r `shouldSatisfy`-            matchHeader "Allow" "OPTIONS,GET,HEAD,PATCH"--      it "includes read and delete verbs for deletable views" $ do-        r <- request methodOptions "/projects_view_with_delete_trigger" [] ""-        liftIO $-          simpleHeaders r `shouldSatisfy`-            matchHeader "Allow" "OPTIONS,GET,HEAD,DELETE"
− test/Feature/ProxySpec.hs
@@ -1,13 +0,0 @@-module Feature.ProxySpec where--import Network.Wai (Application)-import Test.Hspec  hiding (pendingWith)--import Protolude-import SpecHelper--spec :: SpecWith ((), Application)-spec =-  describe "GET / with proxy" $-    it "returns a valid openapi spec with proxy" $-      validateOpenApiResponse [("Accept", "application/openapi+json")]
− test/Feature/QueryLimitedSpec.hs
@@ -1,69 +0,0 @@-module Feature.QueryLimitedSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude  hiding (get)-import SpecHelper--spec :: SpecWith ((), Application)-spec =-  describe "Requesting many items with server limits(max-rows) enabled" $ do-    it "restricts results" $-      get "/items?order=id"-        `shouldRespondWith`-          [json| [{"id":1},{"id":2}] |]-          { matchHeaders = ["Content-Range" <:> "0-1/*"] }--    it "respects additional client limiting" $ do-      request methodGet  "/items"-          (rangeHdrs $ ByteRangeFromTo 0 0)-          ""-        `shouldRespondWith`-          [json| [{"id":1}] |]-          { matchHeaders = ["Content-Range" <:> "0-0/*"] }--    it "works on all levels" $-      get "/users?select=id,tasks(id)&order=id.asc&tasks.order=id.asc"-        `shouldRespondWith`-          [json|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]-          { matchHeaders = ["Content-Range" <:> "0-1/*"] }--    it "succeeds in getting parent embeds despite the limit, see #647" $-      get "/tasks?select=id,project:projects(id)&id=gt.5"-        `shouldRespondWith`-          [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]-          { matchHeaders = ["Content-Range" <:> "0-1/*"] }--    it "can offset the parent embed, being consistent with the other embed types" $-      get "/tasks?select=id,project:projects(id)&id=gt.5&project.offset=1"-        `shouldRespondWith`-          [json|[{"id":6,"project":null}, {"id":7,"project":null}]|]-          { matchHeaders = ["Content-Range" <:> "0-1/*"] }--    context "count=estimated" $ do-      it "uses the query planner guess when query rows > maxRows" $-        request methodHead "/getallprojects_view" [("Prefer", "count=estimated")] ""-          `shouldRespondWith`-            ""-            { matchStatus  = 206-            , matchHeaders = ["Content-Range" <:> "0-1/2019"]-            }--      it "gives exact count when query rows <= maxRows" $-        request methodHead "/getallprojects_view?id=lt.3" [("Prefer", "count=estimated")] ""-          `shouldRespondWith`-            ""-            { matchHeaders = ["Content-Range" <:> "0-1/2"] }--      it "only uses the query planner guess if it's indeed greater than the exact count" $-        request methodHead "/get_projects_above_view" [("Prefer", "count=estimated")] ""-          `shouldRespondWith`-            ""-            { matchStatus  = 206-            , matchHeaders = ["Content-Range" <:> "0-1/3"]-            }
− test/Feature/QuerySpec.hs
@@ -1,1108 +0,0 @@-module Feature.QuerySpec where--import Network.Wai      (Application)-import Network.Wai.Test (SResponse (simpleHeaders))--import Network.HTTP.Types-import Test.Hspec          hiding (pendingWith)-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import PostgREST.Config.PgVersion (PgVersion, pgVersion110,-                                   pgVersion112, pgVersion121)-import Protolude                  hiding (get)-import SpecHelper--spec :: PgVersion -> SpecWith ((), Application)-spec actualPgVersion = do--  describe "Querying a table with a column called count" $-    it "should not confuse count column with pg_catalog.count aggregate" $-      get "/has_count_column" `shouldRespondWith` 200--  describe "Querying a table with a column called t" $-    it "should not conflict with internal postgrest table alias" $-      get "/clashing_column?select=t" `shouldRespondWith` 200--  describe "Querying a nonexistent table" $-    it "causes a 404" $-      get "/faketable" `shouldRespondWith` 404--  describe "Filtering response" $ do-    it "matches with equality" $-      get "/items?id=eq.5"-        `shouldRespondWith` [json| [{"id":5}] |]-        { matchHeaders = ["Content-Range" <:> "0-0/*"] }--    it "matches with equality using not operator" $-      get "/items?id=not.eq.5&order=id"-        `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]-        { matchHeaders = ["Content-Range" <:> "0-13/*"] }--    it "matches with more than one condition using not operator" $-      get "/simple_pk?k=like.*yx&extra=not.eq.u" `shouldRespondWith` "[]"--    it "matches with inequality using not operator" $ do-      get "/items?id=not.lt.14&order=id.asc"-        `shouldRespondWith` [json| [{"id":14},{"id":15}] |]-        { matchHeaders = ["Content-Range" <:> "0-1/*"] }-      get "/items?id=not.gt.2&order=id.asc"-        `shouldRespondWith` [json| [{"id":1},{"id":2}] |]-        { matchHeaders = ["Content-Range" <:> "0-1/*"] }--    it "matches items IN" $-      get "/items?id=in.(1,3,5)"-        `shouldRespondWith` [json| [{"id":1},{"id":3},{"id":5}] |]-        { matchHeaders = ["Content-Range" <:> "0-2/*"] }--    it "matches items NOT IN using not operator" $-      get "/items?id=not.in.(2,4,6,7,8,9,10,11,12,13,14,15)"-        `shouldRespondWith` [json| [{"id":1},{"id":3},{"id":5}] |]-        { matchHeaders = ["Content-Range" <:> "0-2/*"] }--    it "matches nulls using not operator" $-      get "/no_pk?a=not.is.null" `shouldRespondWith`-        [json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]-        { matchHeaders = [matchContentTypeJson] }--    it "matches nulls in varchar and numeric fields alike" $ do-      get "/no_pk?a=is.null" `shouldRespondWith`-        [json| [{"a": null, "b": null}] |]-        { matchHeaders = [matchContentTypeJson] }--      get "/nullable_integer?a=is.null" `shouldRespondWith` [json|[{"a":null}]|]--    it "matches with trilean values" $ do-      get "/chores?done=is.true" `shouldRespondWith`-        [json| [{"id": 1, "name": "take out the garbage", "done": true }] |]-        { matchHeaders = [matchContentTypeJson] }--      get "/chores?done=is.false" `shouldRespondWith`-        [json| [{"id": 2, "name": "do the laundry", "done": false }] |]-        { matchHeaders = [matchContentTypeJson] }--      get "/chores?done=is.unknown" `shouldRespondWith`-        [json| [{"id": 3, "name": "wash the dishes", "done": null }] |]-        { matchHeaders = [matchContentTypeJson] }--    it "fails if 'is' used and there's no null or trilean value" $ do-      get "/chores?done=is.nil" `shouldRespondWith` 400-      get "/chores?done=is.ok"  `shouldRespondWith` 400--    it "matches with like" $ do-      get "/simple_pk?k=like.*yx" `shouldRespondWith`-        [json|[{"k":"xyyx","extra":"u"}]|]-      get "/simple_pk?k=like.xy*" `shouldRespondWith`-        [json|[{"k":"xyyx","extra":"u"}]|]-      get "/simple_pk?k=like.*YY*" `shouldRespondWith`-        [json|[{"k":"xYYx","extra":"v"}]|]--    it "matches with like using not operator" $-      get "/simple_pk?k=not.like.*yx" `shouldRespondWith`-        [json|[{"k":"xYYx","extra":"v"}]|]--    it "matches with ilike" $ do-      get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`-        [json|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|]-        { matchHeaders = [matchContentTypeJson] }-      get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`-        [json|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "matches with ilike using not operator" $-      get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]"--    describe "Full text search operator" $ do-      it "finds matches with to_tsquery" $-        get "/tsearch?text_search_vector=fts.impossible" `shouldRespondWith`-          [json| [{"text_search_vector": "'fun':5 'imposs':9 'kind':3" }] |]-          { matchHeaders = [matchContentTypeJson] }--      it "can use lexeme boolean operators(&=%26, |=%7C, !) in to_tsquery" $ do-        get "/tsearch?text_search_vector=fts.fun%26possible" `shouldRespondWith`-          [json| [ {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/tsearch?text_search_vector=fts.impossible%7Cpossible" `shouldRespondWith`-          [json| [-          {"text_search_vector": "'fun':5 'imposs':9 'kind':3"},-          {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/tsearch?text_search_vector=fts.fun%26!possible" `shouldRespondWith`-          [json| [ {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}] |]-          { matchHeaders = [matchContentTypeJson] }--      it "finds matches with plainto_tsquery" $-        get "/tsearch?text_search_vector=plfts.The%20Fat%20Rats" `shouldRespondWith`-          [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]-          { matchHeaders = [matchContentTypeJson] }--      when (actualPgVersion >= pgVersion112) $ do-        it "finds matches with websearch_to_tsquery" $-            get "/tsearch?text_search_vector=wfts.The%20Fat%20Rats" `shouldRespondWith`-                [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]-                { matchHeaders = [matchContentTypeJson] }--        it "can use boolean operators(and, or, -) in websearch_to_tsquery" $ do-          get "/tsearch?text_search_vector=wfts.fun%20and%20possible"-            `shouldRespondWith`-              [json| [ {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] |]-              { matchHeaders = [matchContentTypeJson] }-          get "/tsearch?text_search_vector=wfts.impossible%20or%20possible"-            `shouldRespondWith`-              [json| [-                {"text_search_vector": "'fun':5 'imposs':9 'kind':3"},-                {"text_search_vector": "'also':2 'fun':3 'possibl':8"}]-                  |]-              { matchHeaders = [matchContentTypeJson] }-          get "/tsearch?text_search_vector=wfts.fun%20and%20-possible"-            `shouldRespondWith`-              [json| [ {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}] |]-              { matchHeaders = [matchContentTypeJson] }--      it "finds matches with different dictionaries" $ do-        get "/tsearch?text_search_vector=fts(french).amusant" `shouldRespondWith`-          [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/tsearch?text_search_vector=plfts(french).amusant%20impossible" `shouldRespondWith`-          [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |]-          { matchHeaders = [matchContentTypeJson] }--        when (actualPgVersion >= pgVersion112) $-            get "/tsearch?text_search_vector=wfts(french).amusant%20impossible"-                `shouldRespondWith`-                  [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |]-                  { matchHeaders = [matchContentTypeJson] }--      it "can be negated with not operator" $ do-        get "/tsearch?text_search_vector=not.fts.impossible%7Cfat%7Cfun" `shouldRespondWith`-          [json| [-            {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},-            {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/tsearch?text_search_vector=not.fts(english).impossible%7Cfat%7Cfun" `shouldRespondWith`-          [json| [-            {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},-            {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/tsearch?text_search_vector=not.plfts.The%20Fat%20Rats" `shouldRespondWith`-          [json| [-            {"text_search_vector": "'fun':5 'imposs':9 'kind':3"},-            {"text_search_vector": "'also':2 'fun':3 'possibl':8"},-            {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},-            {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]-          { matchHeaders = [matchContentTypeJson] }-        when (actualPgVersion >= pgVersion112) $-            get "/tsearch?text_search_vector=not.wfts(english).impossible%20or%20fat%20or%20fun"-                `shouldRespondWith`-                  [json| [-                    {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},-                    {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]-                  { matchHeaders = [matchContentTypeJson] }--      context "Use of the phraseto_tsquery function" $ do-        it "finds matches" $-          get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith`-            [json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]-            { matchHeaders = [matchContentTypeJson] }--        it "finds matches with different dictionaries" $-          get "/tsearch?text_search_vector=phfts(german).Art%20Spass" `shouldRespondWith`-            [json| [{"text_search_vector": "'art':4 'spass':5 'unmog':7" }] |]-            { matchHeaders = [matchContentTypeJson] }--        it "can be negated with not operator" $-          get "/tsearch?text_search_vector=not.phfts(english).The%20Fat%20Cats" `shouldRespondWith`-            [json| [-              {"text_search_vector": "'fun':5 'imposs':9 'kind':3"},-              {"text_search_vector": "'also':2 'fun':3 'possibl':8"},-              {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},-              {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]-            { matchHeaders = [matchContentTypeJson] }--        it "can be used with or query param" $-          get "/tsearch?or=(text_search_vector.phfts(german).Art%20Spass, text_search_vector.phfts(french).amusant, text_search_vector.fts(english).impossible)" `shouldRespondWith`-            [json|[-              {"text_search_vector": "'fun':5 'imposs':9 'kind':3" },-              {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },-              {"text_search_vector": "'art':4 'spass':5 'unmog':7"}-            ]|] { matchHeaders = [matchContentTypeJson] }--    it "matches with computed column" $-      get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith`-        [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]-        { matchHeaders = [matchContentTypeJson] }--    it "order by computed column" $-      get "/items?order=anti_id.desc" `shouldRespondWith`-        [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]-        { matchHeaders = [matchContentTypeJson] }--    it "cannot access a computed column that is outside of the config schema" $-      get "/items?always_false=is.false" `shouldRespondWith` 400--    it "matches filtering nested items 2" $-      get "/clients?select=id,projects(id,tasks2(id,name))&projects.tasks.name=like.Design*" `shouldRespondWith`-        [json| {-          "hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",-          "message":"Could not find a relationship between projects and tasks2 in the schema cache"}|]-        { matchStatus  = 400-        , matchHeaders = [matchContentTypeJson]-        }--    it "matches filtering nested items" $-      get "/clients?select=id,projects(id,tasks(id,name))&projects.tasks.name=like.Design*" `shouldRespondWith`-        [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1,"name":"Design w7"}]},{"id":2,"tasks":[{"id":3,"name":"Design w10"}]}]},{"id":2,"projects":[{"id":3,"tasks":[{"id":5,"name":"Design IOS"}]},{"id":4,"tasks":[{"id":7,"name":"Design OSX"}]}]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "matches with cs operator" $-      get "/complex_items?select=id&arr_data=cs.{2}" `shouldRespondWith`-        [json|[{"id":2},{"id":3}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "matches with cd operator" $-      get "/complex_items?select=id&arr_data=cd.{1,2,4}" `shouldRespondWith`-        [json|[{"id":1},{"id":2}]|]-        { matchHeaders = [matchContentTypeJson] }--  describe "Shaping response with select parameter" $ do-    it "selectStar works in absense of parameter" $-      get "/complex_items?id=eq.3" `shouldRespondWith`-        [json|[{"id":3,"name":"Three","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}]|]--    it "dash `-` in column names is accepted" $-      get "/complex_items?id=eq.3&select=id,field-with_sep" `shouldRespondWith`-        [json|[{"id":3,"field-with_sep":1}]|]--    it "one simple column" $-      get "/complex_items?select=id" `shouldRespondWith`-        [json| [{"id":1},{"id":2},{"id":3}] |]-        { matchHeaders = [matchContentTypeJson] }--    it "rename simple column" $-      get "/complex_items?id=eq.1&select=myId:id" `shouldRespondWith`-        [json| [{"myId":1}] |]-        { matchHeaders = [matchContentTypeJson] }--    it "one simple column with casting (text)" $-      get "/complex_items?select=id::text" `shouldRespondWith`-        [json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]-        { matchHeaders = [matchContentTypeJson] }--    it "rename simple column with casting" $-      get "/complex_items?id=eq.1&select=myId:id::text" `shouldRespondWith`-        [json| [{"myId":"1"}] |]-        { matchHeaders = [matchContentTypeJson] }--    it "json column" $-      get "/complex_items?id=eq.1&select=settings" `shouldRespondWith`-        [json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]-        { matchHeaders = [matchContentTypeJson] }--    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"} |]-        { matchStatus  = 400-        , matchHeaders = []-        }--    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 and renaming primary key" $-      get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith`-        [json|[-          {"name":"Windows 7","client":{"name": "Microsoft", "clientId": 1}},-          {"name":"Windows 10","client":{"name": "Microsoft", "clientId": 1}},-          {"name":"IOS","client":{"name": "Apple", "clientId": 2}},-          {"name":"OSX","client":{"name": "Apple", "clientId": 2}},-          {"name":"Orphan","client":null}-        ]|]-        { matchHeaders = [matchContentTypeJson] }--    it "requesting parent and specifying/renaming one key of the composite primary key" $ do-      get "/comments?select=*,users_tasks(userId:user_id)" `shouldRespondWith`-        [json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"userId": 2}}]|]-        { matchHeaders = [matchContentTypeJson] }-      get "/comments?select=*,users_tasks(taskId:task_id)" `shouldRespondWith`-        [json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"taskId": 6}}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "requesting parents and children while renaming them" $-      get "/projects?id=eq.1&select=myId:id, name, project_client:clients(*), project_tasks:tasks(id, name)" `shouldRespondWith`-        [json|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "requesting parents and filtering parent columns" $-      get "/projects?id=eq.1&select=id, name, clients(id)" `shouldRespondWith`-        [json|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|]--    it "rows with missing parents are included" $-      get "/projects?id=in.(1,5)&select=id,clients(id)" `shouldRespondWith`-        [json|[{"id":1,"clients":{"id":1}},{"id":5,"clients":null}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "rows with no children return [] instead of null" $-      get "/projects?id=in.(5)&select=id,tasks(id)" `shouldRespondWith`-        [json|[{"id":5,"tasks":[]}]|]--    it "requesting children 2 levels" $-      get "/clients?id=eq.1&select=id,projects(id,tasks(id))" `shouldRespondWith`-        [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "requesting many<->many relation" $-      get "/tasks?select=id,users(id)" `shouldRespondWith`-        [json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "requesting many<->many relation with rename" $-      get "/tasks?id=eq.1&select=id,theUsers:users(id)" `shouldRespondWith`-        [json|[{"id":1,"theUsers":[{"id":1},{"id":3}]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "requesting many<->many relation reverse" $-      get "/users?select=id,tasks(id)" `shouldRespondWith`-        [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 many<->many relation using composite key" $-      get "/files?filename=eq.autoexec.bat&project_id=eq.1&select=filename,users_tasks(user_id,task_id)" `shouldRespondWith`-        [json|[{"filename":"autoexec.bat","users_tasks":[{"user_id":1,"task_id":1},{"user_id":3,"task_id":1}]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "requesting data using many<->many relation defined by composite keys" $-      get "/users_tasks?user_id=eq.1&task_id=eq.1&select=user_id,files(filename,content)" `shouldRespondWith`-        [json|[{"user_id":1,"files":[{"filename":"command.com","content":"#include <unix.h>"},{"filename":"autoexec.bat","content":"@ECHO OFF"},{"filename":"README.md","content":"# make $$$!"}]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "requesting data using many<->many (composite keys) relation using hint" $-      get "/users_tasks?user_id=eq.1&task_id=eq.1&select=user_id,files!touched_files(filename,content)" `shouldRespondWith`-        [json|[{"user_id":1,"files":[{"filename":"command.com","content":"#include <unix.h>"},{"filename":"autoexec.bat","content":"@ECHO OFF"},{"filename":"README.md","content":"# make $$$!"}]}]|]-        { 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] }--    describe "computed columns" $ do-      it "computed column on table" $-        get "/items?id=eq.1&select=id,always_true" `shouldRespondWith`-          [json|[{"id":1,"always_true":true}]|]-          { matchHeaders = [matchContentTypeJson] }--      it "computed column on rpc" $-        get "/rpc/search?id=1&select=id,always_true" `shouldRespondWith`-          [json|[{"id":1,"always_true":true}]|]-          { matchHeaders = [matchContentTypeJson] }--      it "overloaded computed columns on both tables" $ do-        get "/items?id=eq.1&select=id,computed_overload" `shouldRespondWith`-          [json|[{"id":1,"computed_overload":true}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/items2?id=eq.1&select=id,computed_overload" `shouldRespondWith`-          [json|[{"id":1,"computed_overload":true}]|]-          { matchHeaders = [matchContentTypeJson] }--      it "overloaded computed column on rpc" $-        get "/rpc/search?id=1&select=id,computed_overload" `shouldRespondWith`-          [json|[{"id":1,"computed_overload":true}]|]-          { matchHeaders = [matchContentTypeJson] }--    when (actualPgVersion >= pgVersion110) $ do-      describe "partitioned tables embedding" $ do-        it "can request a table as parent from a partitioned table" $-          get "/car_models?name=in.(DeLorean,Murcielago)&select=name,year,car_brands(name)&order=name.asc" `shouldRespondWith`-            [json|-              [{"name":"DeLorean","year":1981,"car_brands":{"name":"DMC"}},-               {"name":"Murcielago","year":2001,"car_brands":{"name":"Lamborghini"}}] |]-            { matchHeaders = [matchContentTypeJson] }--        it "can request partitioned tables as children from a table" $-          get "/car_brands?select=name,car_models(name,year)&order=name.asc&car_models.order=name.asc" `shouldRespondWith`-            [json|-              [{"name":"DMC","car_models":[{"name":"DeLorean","year":1981}]},-               {"name":"Ferrari","car_models":[{"name":"F310-B","year":1997}]},-               {"name":"Lamborghini","car_models":[{"name":"Murcielago","year":2001},{"name":"Veneno","year":2013}]}] |]-            { matchHeaders = [matchContentTypeJson] }--        when (actualPgVersion >= pgVersion121) $ do-          it "can request tables as children from a partitioned table" $-            get "/car_models?name=in.(DeLorean,F310-B)&select=name,year,car_racers(name)&order=name.asc" `shouldRespondWith`-              [json|-                [{"name":"DeLorean","year":1981,"car_racers":[]},-                 {"name":"F310-B","year":1997,"car_racers":[{"name":"Michael Schumacher"}]}] |]-              { matchHeaders = [matchContentTypeJson] }--          it "can request a partitioned table as parent from a table" $-            get "/car_racers?select=name,car_models(name,year)&order=name.asc" `shouldRespondWith`-              [json|-                [{"name":"Alain Prost","car_models":null},-                 {"name":"Michael Schumacher","car_models":{"name":"F310-B","year":1997}}] |]-              { matchHeaders = [matchContentTypeJson] }--          it "can request partitioned tables as children from a partitioned table" $-            get "/car_models?name=in.(DeLorean,Murcielago,Veneno)&select=name,year,car_model_sales(date,quantity)&order=name.asc" `shouldRespondWith`-              [json|-                [{"name":"DeLorean","year":1981,"car_model_sales":[{"date":"2021-01-14","quantity":7},{"date":"2021-01-15","quantity":9}]},-                 {"name":"Murcielago","year":2001,"car_model_sales":[{"date":"2021-02-11","quantity":1},{"date":"2021-02-12","quantity":3}]},-                 {"name":"Veneno","year":2013,"car_model_sales":[]}] |]-              { matchHeaders = [matchContentTypeJson] }--          it "can request a partitioned table as parent from a partitioned table" $ do-            get "/car_model_sales?date=in.(2021-01-15,2021-02-11)&select=date,quantity,car_models(name,year)&order=date.asc" `shouldRespondWith`-              [json|-                [{"date":"2021-01-15","quantity":9,"car_models":{"name":"DeLorean","year":1981}},-                 {"date":"2021-02-11","quantity":1,"car_models":{"name":"Murcielago","year":2001}}] |]-              { matchHeaders = [matchContentTypeJson] }--          it "can request many to many relationships between partitioned tables ignoring the intermediate table partitions" $-            get "/car_models?select=name,year,car_dealers(name,city)&order=name.asc&limit=4" `shouldRespondWith`-              [json|-                [{"name":"DeLorean","year":1981,"car_dealers":[{"name":"Springfield Cars S.A.","city":"Springfield"}]},-                 {"name":"F310-B","year":1997,"car_dealers":[]},-                 {"name":"Murcielago","year":2001,"car_dealers":[{"name":"The Best Deals S.A.","city":"Franklin"}]},-                 {"name":"Veneno","year":2013,"car_dealers":[]}] |]-              { matchStatus  = 200-              , matchHeaders = [matchContentTypeJson]-              }--          it "cannot request partitions as children from a partitioned table" $-            get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith`-              [json|-                {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",-                 "message":"Could not find a relationship between car_models and car_model_sales_202101 in the schema cache"} |]-              { matchStatus  = 400-              , matchHeaders = [matchContentTypeJson]-              }--          it "cannot request a partitioned table as parent from a partition" $-            get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`-              [json|-                {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",-                 "message":"Could not find a relationship between car_model_sales_202101 and car_models in the schema cache"} |]-              { matchStatus  = 400-              , matchHeaders = [matchContentTypeJson]-              }--          it "cannot request a partition as parent from a partitioned table" $-            get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith`-              [json|-                {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",-                 "message":"Could not find a relationship between car_model_sales and car_models_default in the schema cache"} |]-              { matchStatus  = 400-              , matchHeaders = [matchContentTypeJson]-              }--          it "cannot request partitioned tables as children from a partition" $-            get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`-              [json|-                {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",-                 "message":"Could not find a relationship between car_models_default and car_model_sales in the schema cache"} |]-              { matchStatus  = 400-              , matchHeaders = [matchContentTypeJson]-              }--    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 detect fk relations through materialized views to tables in the public schema" $-        get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200--      it "can request two parents" $-        get "/articleStars?select=createdAt,article:articles(id),user:users(name)&limit=1"-          `shouldRespondWith`-            [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"id": 1},"user":{"name": "Angela Martin"}}]|]--      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:authors(name)&id=eq.5" `shouldRespondWith`-          [json|[ { "title": "Farenheit 451", "author": { "name": "Ray Bradbury" } } ]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/forties_books?select=title,author:authors(name)&limit=1" `shouldRespondWith`-          [json|[ { "title": "1984", "author": { "name": "George Orwell" } } ]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/fifties_books?select=title,author:authors(name)&limit=1" `shouldRespondWith`-          [json|[ { "title": "The Catcher in the Rye", "author": { "name": "J.D. Salinger" } } ]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/sixties_books?select=title,author:authors(name)&limit=1" `shouldRespondWith`-          [json|[ { "title": "To Kill a Mockingbird", "author": { "name": "Harper Lee" } } ]|]-          { matchHeaders = [matchContentTypeJson] }--      it "can detect fk relations through multiple views recursively when all views are in api schema" $ do-        get "/consumers_view_view?select=*,orders_view(*)" `shouldRespondWith` 200--      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 subselects in a function call" $-        get "/authors_have_book_in_decade2?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"}]} ]|]--      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:clients(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] }--      it "can embed a view that has a subselect containing a select in a where" $-        get "/authors_w_entities?select=name,entities,books(title)&id=eq.1" `shouldRespondWith`-          [json| [{"name":"George Orwell","entities":[3, 4],"books":[{"title":"1984"}]}] |]-          { matchHeaders = [matchContentTypeJson] }--    describe "aliased embeds" $ do-      it "works with child relation" $-        get "/space?select=id,zones:zone(id,name),stores:zone(id,name)&zones.zone_type_id=eq.2&stores.zone_type_id=eq.3" `shouldRespondWith`-          [json|[-            { "id":1,-              "zones": [ {"id":1,"name":"zone 1"}, {"id":2,"name":"zone 2"}],-              "stores": [ {"id":3,"name":"store 3"}, {"id":4,"name":"store 4"}]}-          ]|] { matchHeaders = [matchContentTypeJson] }--      it "works with many to many relation" $-        get "/users?select=id,designTasks:tasks(id,name),codeTasks:tasks(id,name)&designTasks.name=like.*Design*&codeTasks.name=like.*Code*" `shouldRespondWith`-          [json|[-             { "id":1,-               "designTasks":[ { "id":1, "name":"Design w7" }, { "id":3, "name":"Design w10" } ],-               "codeTasks":[ { "id":2, "name":"Code w7" }, { "id":4, "name":"Code w10" } ] },-             { "id":2,-               "designTasks":[ { "id":5, "name":"Design IOS" }, { "id":7, "name":"Design OSX" } ],-               "codeTasks":[ { "id":6, "name":"Code IOS" } ] },-             { "id":3,-               "designTasks":[ { "id":1, "name":"Design w7" }, { "id":5, "name":"Design IOS" } ],-               "codeTasks":[ ] }-          ]|] { matchHeaders = [matchContentTypeJson] }--      it "works with an aliased child plus non aliased child" $-        get "/projects?select=id,name,designTasks:tasks(name,users(id,name))&designTasks.name=like.*Design*&designTasks.users.id=in.(1,2)" `shouldRespondWith`-          [json|[-            {-              "id":1, "name":"Windows 7",-              "designTasks":[ { "name":"Design w7", "users":[ { "id":1, "name":"Angela Martin" } ] } ] },-            {-              "id":2, "name":"Windows 10",-              "designTasks":[ { "name":"Design w10", "users":[ { "id":1, "name":"Angela Martin" } ] } ] },-            {-              "id":3, "name":"IOS",-              "designTasks":[ { "name":"Design IOS", "users":[ { "id":2, "name":"Michael Scott" } ] } ] },-            {-              "id":4, "name":"OSX",-              "designTasks":[ { "name":"Design OSX", "users":[ { "id":2, "name":"Michael Scott" } ] } ] },-            {-              "id":5, "name":"Orphan",-              "designTasks":[ ] }-          ]|] { matchHeaders = [matchContentTypeJson] }--      it "works with two aliased children embeds plus and/or" $-        get "/entities?select=id,children:child_entities(id,gChildren:grandchild_entities(id))&children.and=(id.in.(1,2,3))&children.gChildren.or=(id.eq.1,id.eq.2)" `shouldRespondWith`-          [json|[-            { "id":1,-              "children":[-                {"id":1,"gChildren":[{"id":1}, {"id":2}]},-                {"id":2,"gChildren":[]}]},-            { "id":2,-              "children":[-                {"id":3,"gChildren":[]}]},-            { "id":3,"children":[]},-            { "id":4,"children":[]}-          ]|] { matchHeaders = [matchContentTypeJson] }--  describe "ordering response" $ do-    it "by a column asc" $-      get "/items?id=lte.2&order=id.asc"-        `shouldRespondWith` [json| [{"id":1},{"id":2}] |]-        { matchStatus  = 200-        , matchHeaders = ["Content-Range" <:> "0-1/*"]-        }---    it "by a column desc" $-      get "/items?id=lte.2&order=id.desc"-        `shouldRespondWith` [json| [{"id":2},{"id":1}] |]-        { matchStatus  = 200-        , matchHeaders = ["Content-Range" <:> "0-1/*"]-        }--    it "by a column with nulls first" $-      get "/no_pk?order=a.nullsfirst"-        `shouldRespondWith` [json| [{"a":null,"b":null},-                              {"a":"1","b":"0"},-                              {"a":"2","b":"0"}-                              ] |]-        { matchStatus = 200-        , matchHeaders = ["Content-Range" <:> "0-2/*"]-        }--    it "by a column asc with nulls last" $-      get "/no_pk?order=a.asc.nullslast"-        `shouldRespondWith` [json| [{"a":"1","b":"0"},-                              {"a":"2","b":"0"},-                              {"a":null,"b":null}] |]-        { matchStatus = 200-        , matchHeaders = ["Content-Range" <:> "0-2/*"]-        }--    it "by a column desc with nulls first" $-      get "/no_pk?order=a.desc.nullsfirst"-        `shouldRespondWith` [json| [{"a":null,"b":null},-                              {"a":"2","b":"0"},-                              {"a":"1","b":"0"}] |]-        { matchStatus = 200-        , matchHeaders = ["Content-Range" <:> "0-2/*"]-        }--    it "by a column desc with nulls last" $-      get "/no_pk?order=a.desc.nullslast"-        `shouldRespondWith` [json| [{"a":"2","b":"0"},-                              {"a":"1","b":"0"},-                              {"a":null,"b":null}] |]-        { matchStatus = 200-        , matchHeaders = ["Content-Range" <:> "0-2/*"]-        }--    it "by two columns with nulls and direction specified" $-      get "/projects?select=client_id,id,name&order=client_id.desc.nullslast,id.desc"-        `shouldRespondWith` [json|-          [{"client_id":2,"id":4,"name":"OSX"},-           {"client_id":2,"id":3,"name":"IOS"},-           {"client_id":1,"id":2,"name":"Windows 10"},-           {"client_id":1,"id":1,"name":"Windows 7"},-           {"client_id":null,"id":5,"name":"Orphan"}]-        |]-        { matchStatus  = 200-        , matchHeaders = ["Content-Range" <:> "0-4/*"]-        }--    it "by a column with no direction or nulls specified" $-      get "/items?id=lte.2&order=id"-        `shouldRespondWith` [json| [{"id":1},{"id":2}] |]-        { matchStatus  = 200-        , matchHeaders = ["Content-Range" <:> "0-1/*"]-        }--    it "without other constraints" $-      get "/items?order=id.asc" `shouldRespondWith` 200--    it "ordering embeded entities" $-      get "/projects?id=eq.1&select=id, name, tasks(id, name)&tasks.order=name.asc" `shouldRespondWith`-        [json|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "ordering embeded entities with alias" $-      get "/projects?id=eq.1&select=id, name, the_tasks:tasks(id, name)&tasks.order=name.asc" `shouldRespondWith`-        [json|[{"id":1,"name":"Windows 7","the_tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "ordering embeded entities, two levels" $-      get "/projects?id=eq.1&select=id, name, tasks(id, name, users(id, name))&tasks.order=name.asc&tasks.users.order=name.desc" `shouldRespondWith`-        [json|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7","users":[{"id":1,"name":"Angela Martin"}]},{"id":1,"name":"Design w7","users":[{"id":3,"name":"Dwight Schrute"},{"id":1,"name":"Angela Martin"}]}]}]|]-        { matchHeaders = [matchContentTypeJson] }--    it "ordering embeded parents does not break things" $-      get "/projects?id=eq.1&select=id, name, clients(id, name)&clients.order=name.asc" `shouldRespondWith`-        [json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"}}]|]--    context "order syntax errors" $ do-      it "gives meaningful error messages when asc/desc/nulls{first,last} are misspelled" $ do-        get "/items?order=id.ac" `shouldRespondWith`-          [json|{"details":"unexpected \"c\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.ac)\" (line 1, column 4)"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson]-          }-        get "/items?order=id.descc" `shouldRespondWith`-          [json|{"details":"unexpected 'c' expecting delimiter (.), \",\" or end of input","message":"\"failed to parse order (id.descc)\" (line 1, column 8)"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson]-          }-        get "/items?order=id.nulsfist" `shouldRespondWith`-          [json|{"details":"unexpected \"n\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.nulsfist)\" (line 1, column 4)"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson]-          }-        get "/items?order=id.nullslasttt" `shouldRespondWith`-          [json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.nullslasttt)\" (line 1, column 13)"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson]-          }-        get "/items?order=id.smth34" `shouldRespondWith`-          [json|{"details":"unexpected \"s\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.smth34)\" (line 1, column 4)"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson]-          }--      it "gives meaningful error messages when nulls{first,last} are misspelled after asc/desc" $ do-        get "/items?order=id.asc.nlsfst" `shouldRespondWith`-          [json|{"details":"unexpected \"l\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.nlsfst)\" (line 1, column 8)"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson]-          }-        get "/items?order=id.asc.nullslasttt" `shouldRespondWith`-          [json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.asc.nullslasttt)\" (line 1, column 17)"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson]-          }-        get "/items?order=id.asc.smth34" `shouldRespondWith`-          [json|{"details":"unexpected \"s\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.smth34)\" (line 1, column 8)"}|]-          { matchStatus  = 400-          , matchHeaders = [matchContentTypeJson]-          }--  describe "Accept headers" $ do-    it "should respond an unknown accept type with 415" $-      request methodGet "/simple_pk"-              (acceptHdrs "text/unknowntype") ""-        `shouldRespondWith`-        [json|{"message":"None of these Content-Types are available: text/unknowntype"}|]-        { matchStatus  = 415-        , matchHeaders = [matchContentTypeJson]-        }--    it "should respond correctly to */* in accept header" $-      request methodGet "/simple_pk"-              (acceptHdrs "*/*") ""-        `shouldRespondWith` 200--    it "*/* should rescue an unknown type" $-      request methodGet "/simple_pk"-              (acceptHdrs "text/unknowntype, */*") ""-        `shouldRespondWith` 200--    it "specific available preference should override */*" $ do-      r <- request methodGet "/simple_pk"-              (acceptHdrs "text/csv, */*") ""-      liftIO $ do-        let respHeaders = simpleHeaders r-        respHeaders `shouldSatisfy` matchHeader-          "Content-Type" "text/csv; charset=utf-8"--    it "honors client preference even when opposite of server preference" $ do-      r <- request methodGet "/simple_pk"-              (acceptHdrs "text/csv, application/json") ""-      liftIO $ do-        let respHeaders = simpleHeaders r-        respHeaders `shouldSatisfy` matchHeader-          "Content-Type" "text/csv; charset=utf-8"--    it "should respond correctly to multiple types in accept header" $-      request methodGet "/simple_pk"-              (acceptHdrs "text/unknowntype, text/csv") ""-        `shouldRespondWith` 200--    it "should respond with CSV to 'text/csv' request" $-      request methodGet "/simple_pk"-              (acceptHdrs "text/csv; version=1") ""-        `shouldRespondWith` "k,extra\nxyyx,u\nxYYx,v"-        { matchStatus  = 200-        , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]-        }--  describe "Canonical location" $ do-    it "Sets Content-Location with alphabetized params" $-      get "/no_pk?b=eq.1&a=eq.1"-        `shouldRespondWith` "[]"-        { matchStatus  = 200-        , matchHeaders = ["Content-Location" <:> "/no_pk?a=eq.1&b=eq.1"]-        }--    it "Omits question mark when there are no params" $ do-      r <- get "/simple_pk"-      liftIO $ do-        let respHeaders = simpleHeaders r-        respHeaders `shouldSatisfy` matchHeader-          "Content-Location" "/simple_pk"--  describe "weird requests" $ do-    it "can query as normal" $ do-      get "/Escap3e;" `shouldRespondWith`-        [json| [{"so6meIdColumn":1},{"so6meIdColumn":2},{"so6meIdColumn":3},{"so6meIdColumn":4},{"so6meIdColumn":5}] |]-        { matchHeaders = [matchContentTypeJson] }-      get "/ghostBusters" `shouldRespondWith`-        [json| [{"escapeId":1},{"escapeId":3},{"escapeId":5}] |]-        { matchHeaders = [matchContentTypeJson] }--    it "fails if an operator is not given" $-      get "/ghostBusters?id=0" `shouldRespondWith` [json| {"details":"unexpected \"0\" expecting \"not\" or operator (eq, gt, ...)","message":"\"failed to parse filter (0)\" (line 1, column 1)"} |]-        { matchStatus  = 400-        , matchHeaders = [matchContentTypeJson]-        }--    it "will embed a collection" $-      get "/Escap3e;?select=ghostBusters(*)" `shouldRespondWith`-        [json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":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] }--  context "binary output" $ do-    it "can query if a single column is selected" $-      request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") ""-        `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"-        { matchStatus = 200-        , matchHeaders = ["Content-Type" <:> "application/octet-stream"]-        }--    it "can get raw output with Accept: text/plain" $-      request methodGet "/projects?select=name&id=eq.1" (acceptHdrs "text/plain") ""-        `shouldRespondWith` "Windows 7"-        { matchStatus = 200-        , matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]-        }--    it "fails if a single column is not selected" $ do-      request methodGet "/images?select=img,name&name=eq.A.png" (acceptHdrs "application/octet-stream") ""-        `shouldRespondWith`-          [json| {"message":"application/octet-stream requested but more than one column was selected"} |]-          { matchStatus = 406 }--      request methodGet "/images?select=*&name=eq.A.png"-          (acceptHdrs "application/octet-stream")-          ""-        `shouldRespondWith`-          [json| {"message":"application/octet-stream requested but more than one column was selected"} |]-          { matchStatus = 406 }--      request methodGet "/images?name=eq.A.png"-          (acceptHdrs "application/octet-stream")-          ""-        `shouldRespondWith`-          [json| {"message":"application/octet-stream requested but more than one column was selected"} |]-          { matchStatus = 406 }--    it "concatenates results if more than one row is returned" $-      request methodGet "/images_base64?select=img&name=in.(A.png,B.png)" (acceptHdrs "application/octet-stream") ""-        `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="-        { matchStatus = 200-        , matchHeaders = ["Content-Type" <:> "application/octet-stream"]-        }--  describe "values with quotes in IN and NOT IN" $ do-    it "succeeds when only quoted values are present" $ do-      get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\")" `shouldRespondWith`-        [json| [{"name":"Hebdon, John"}] |]-        { matchHeaders = [matchContentTypeJson] }-      get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")" `shouldRespondWith`-        [json| [{"name":"Hebdon, John"},{"name":"Williams, Mary"},{"name":"Smith, Joseph"}] |]-        { matchHeaders = [matchContentTypeJson] }-      get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")&limit=3" `shouldRespondWith`-        [json| [ { "name": "David White" }, { "name": "Larry Thompson" }, { "name": "Double O Seven(007)" }] |]-        { matchHeaders = [matchContentTypeJson] }--    it "succeeds w/ and w/o quoted values" $ do-      get "/w_or_wo_comma_names?name=in.(David White,\"Hebdon, John\")" `shouldRespondWith`-        [json| [{"name":"Hebdon, John"},{"name":"David White"}] |]-        { matchHeaders = [matchContentTypeJson] }-      get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",Larry Thompson,\"Smith, Joseph\")&limit=3" `shouldRespondWith`-        [json| [ { "name": "Williams, Mary" }, { "name": "David White" }, { "name": "Double O Seven(007)" }] |]-        { matchHeaders = [matchContentTypeJson] }-      get "/w_or_wo_comma_names?name=in.(\"Double O Seven(007)\")" `shouldRespondWith`-        [json| [{"name":"Double O Seven(007)"}] |]-        { matchHeaders = [matchContentTypeJson] }--    context "escaped chars" $ do-      it "accepts escaped double quotes" $-        get "/w_or_wo_comma_names?name=in.(\"Double\\\"Quote\\\"McGraw\\\"\")" `shouldRespondWith`-          [json| [ { "name": "Double\"Quote\"McGraw\"" } ] |]-          { matchHeaders = [matchContentTypeJson] }--      it "accepts escaped backslashes" $ do-        get "/w_or_wo_comma_names?name=in.(\"\\\\\")" `shouldRespondWith`-          [json| [{ "name": "\\" }] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/w_or_wo_comma_names?name=in.(\"/\\\\Slash/\\\\Beast/\\\\\")" `shouldRespondWith`-          [json| [ { "name": "/\\Slash/\\Beast/\\" } ] |]-          { matchHeaders = [matchContentTypeJson] }--      it "passes any escaped char as the same char" $-        get "/w_or_wo_comma_names?name=in.(\"D\\a\\vid W\\h\\ite\")" `shouldRespondWith`-          [json| [{ "name": "David White" }] |]-          { matchHeaders = [matchContentTypeJson] }--  describe "IN values without quotes" $ do-    it "accepts single double quotes as values" $ do-      get "/w_or_wo_comma_names?name=in.(\")" `shouldRespondWith`-        [json| [{ "name": "\"" }] |]-        { matchHeaders = [matchContentTypeJson] }-      get "/w_or_wo_comma_names?name=in.(Double\"Quote\"McGraw\")" `shouldRespondWith`-        [json| [ { "name": "Double\"Quote\"McGraw\"" } ] |]-        { matchHeaders = [matchContentTypeJson] }--    it "accepts backslashes as values" $ do-      get "/w_or_wo_comma_names?name=in.(\\)" `shouldRespondWith`-        [json| [{ "name": "\\" }] |]-        { matchHeaders = [matchContentTypeJson] }-      get "/w_or_wo_comma_names?name=in.(/\\Slash/\\Beast/\\)" `shouldRespondWith`-        [json| [ { "name": "/\\Slash/\\Beast/\\" } ] |]-        { matchHeaders = [matchContentTypeJson] }--  describe "IN and NOT IN empty set" $ do-    context "returns an empty result for IN when no value is present" $ do-      it "works for integer" $-        get "/items_with_different_col_types?int_data=in.()" `shouldRespondWith`-          [json| [] |] { matchHeaders = [matchContentTypeJson] }-      it "works for text" $-        get "/items_with_different_col_types?text_data=in.()" `shouldRespondWith`-          [json| [] |] { matchHeaders = [matchContentTypeJson] }-      it "works for bool" $-        get "/items_with_different_col_types?bool_data=in.()" `shouldRespondWith`-          [json| [] |] { matchHeaders = [matchContentTypeJson] }-      it "works for bytea" $-        get "/items_with_different_col_types?bin_data=in.()" `shouldRespondWith`-          [json| [] |] { matchHeaders = [matchContentTypeJson] }-      it "works for char" $-        get "/items_with_different_col_types?char_data=in.()" `shouldRespondWith`-          [json| [] |] { matchHeaders = [matchContentTypeJson] }-      it "works for date" $-        get "/items_with_different_col_types?date_data=in.()" `shouldRespondWith`-          [json| [] |] { matchHeaders = [matchContentTypeJson] }-      it "works for real" $-        get "/items_with_different_col_types?real_data=in.()" `shouldRespondWith`-          [json| [] |] { matchHeaders = [matchContentTypeJson] }-      it "works for time" $-        get "/items_with_different_col_types?time_data=in.()" `shouldRespondWith`-          [json| [] |] { matchHeaders = [matchContentTypeJson] }--    it "returns all results for not.in when no value is present" $-      get "/items_with_different_col_types?int_data=not.in.()&select=int_data" `shouldRespondWith`-        [json| [{int_data: 1}] |] { matchHeaders = [matchContentTypeJson] }--    it "returns an empty result ignoring spaces" $-      get "/items_with_different_col_types?int_data=in.(    )" `shouldRespondWith`-        [json| [] |] { matchHeaders = [matchContentTypeJson] }--    it "only returns an empty result set if the in value is empty" $-      get "/items_with_different_col_types?int_data=in.( ,3,4)"-        `shouldRespondWith` (-        if actualPgVersion >= pgVersion121 then-        [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for type integer: \"\""} |]-        else-        [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"\""} |]-                            )-        { matchStatus = 400-        , matchHeaders = [matchContentTypeJson]-        }--  describe "Embedding when column name = table name" $ do-    it "works with child embeds" $-      get "/being?select=*,descendant(*)&limit=1" `shouldRespondWith`-        [json|[{"being":1,"descendant":[{"descendant":1,"being":1},{"descendant":2,"being":1},{"descendant":3,"being":1}]}]|]-        { matchHeaders = [matchContentTypeJson] }-    it "works with many to many embeds" $-      get "/being?select=*,part(*)&limit=1" `shouldRespondWith`-        [json|[{"being":1,"part":[{"part":1}]}]|]-        { matchHeaders = [matchContentTypeJson] }--  describe "Foreign table" $ do-    it "can be queried by using regular filters" $-      get "/projects_dump?id=in.(1,2,3)" `shouldRespondWith`-        [json| [{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}, {"id":3,"name":"IOS","client_id":2}]|]-        { matchHeaders = [matchContentTypeJson] }-    it "can be queried with select, order and limit" $-      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--  context "VIEW that has a source FK based on a UNIQUE key" $-    it "can be embedded" $-      get "/referrals?select=site,link:pages(url)" `shouldRespondWith`-        [json| [-         {"site":"github.com",     "link":{"url":"http://postgrest.org/en/v6.0/api.html"}},-         {"site":"hub.docker.com", "link":{"url":"http://postgrest.org/en/v6.0/admin.html"}}-        ]|]-        { matchHeaders = [matchContentTypeJson] }--  it "shouldn't produce a Content-Profile header since only a single schema is exposed" $ do-    r <- get "/items"-    liftIO $ do-      let respHeaders = simpleHeaders r-      respHeaders `shouldSatisfy` noProfileHeader
− test/Feature/RangeSpec.hs
@@ -1,352 +0,0 @@-module Feature.RangeSpec where--import qualified Data.ByteString.Lazy as BL--import Network.Wai      (Application)-import Network.Wai.Test (SResponse (simpleHeaders, simpleStatus))--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude  hiding (get)-import SpecHelper--defaultRange :: BL.ByteString-defaultRange = [json| { "min": 0, "max": 15 } |]--emptyRange :: BL.ByteString-emptyRange = [json| { "min": 2, "max": 2 } |]--spec :: SpecWith ((), Application)-spec = do-  describe "POST /rpc/getitemrange" $ do-    context "without range headers" $ do-      context "with response under server size limit" $-        it "returns whole range with status 200" $-           post "/rpc/getitemrange" defaultRange `shouldRespondWith` 200--      context "when I don't want the count" $ do-        it "returns range Content-Range with */* for empty range" $-          request methodPost "/rpc/getitemrange" [] emptyRange-            `shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}--        it "returns range Content-Range with range/*" $-          post "/rpc/getitemrange?order=id"-              defaultRange-            `shouldRespondWith`-              [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]-              { matchHeaders = ["Content-Range" <:> "0-14/*"] }--    context "with range headers" $ do-      context "of acceptable range" $ do-        it "succeeds with partial content" $ do-          r <- request methodPost  "/rpc/getitemrange"-                       (rangeHdrs $ ByteRangeFromTo 0 1) defaultRange-          liftIO $ do-            simpleHeaders r `shouldSatisfy`-              matchHeader "Content-Range" "0-1/*"-            simpleStatus r `shouldBe` ok200--        it "understands open-ended ranges" $-          request methodPost "/rpc/getitemrange"-                  (rangeHdrs $ ByteRangeFrom 0) defaultRange-            `shouldRespondWith` 200--        it "returns an empty body when there are no results" $-          request methodPost "/rpc/getitemrange"-                  (rangeHdrs $ ByteRangeFromTo 0 1) emptyRange-            `shouldRespondWith` "[]"-            { matchStatus  = 200-            , matchHeaders = ["Content-Range" <:> "*/*"]-            }--        it "allows one-item requests" $ do-          r <- request methodPost  "/rpc/getitemrange"-                       (rangeHdrs $ ByteRangeFromTo 0 0) defaultRange-          liftIO $ do-            simpleHeaders r `shouldSatisfy`-              matchHeader "Content-Range" "0-0/*"-            simpleStatus r `shouldBe` ok200--        it "handles ranges beyond collection length via truncation" $ do-          r <- request methodPost  "/rpc/getitemrange"-                       (rangeHdrs $ ByteRangeFromTo 10 100) defaultRange-          liftIO $ do-            simpleHeaders r `shouldSatisfy`-              matchHeader "Content-Range" "10-14/*"-            simpleStatus r `shouldBe` ok200--      context "of invalid range" $ do-        it "fails with 416 for offside range" $-          request methodPost  "/rpc/getitemrange"-                  (rangeHdrs $ ByteRangeFromTo 1 0) emptyRange-            `shouldRespondWith` 416--        it "refuses a range with nonzero start when there are no items" $-          request methodPost "/rpc/getitemrange"-                  (rangeHdrsWithCount $ ByteRangeFromTo 1 2) emptyRange-            `shouldRespondWith` "[]"-            { matchStatus  = 416-            , matchHeaders = ["Content-Range" <:> "*/0"]-            }--        it "refuses a range requesting start past last item" $-          request methodPost "/rpc/getitemrange"-                  (rangeHdrsWithCount $ ByteRangeFromTo 100 199) defaultRange-            `shouldRespondWith` "[]"-            { matchStatus  = 416-            , matchHeaders = ["Content-Range" <:> "*/15"]-            }--  describe "GET /items" $ do-    context "without range headers" $ do-      context "with response under server size limit" $-        it "returns whole range with status 200" $-          get "/items" `shouldRespondWith` 200--      context "when I don't want the count" $ do-        it "returns range Content-Range with /*" $-          request methodGet "/menagerie"-                  [("Prefer", "count=none")] ""-            `shouldRespondWith` "[]"-            { matchStatus  = 200-            , matchHeaders = ["Content-Range" <:> "*/*"]-            }--        it "returns range Content-Range with range/*" $-          request methodGet "/items?order=id"-                  [("Prefer", "count=none")] ""-            `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]-            { matchHeaders = ["Content-Range" <:> "0-14/*"] }--        it "returns range Content-Range with range/* even using other filters" $-          request methodGet "/items?id=eq.1&order=id"-                  [("Prefer", "count=none")] ""-            `shouldRespondWith` [json| [{"id":1}] |]-            { matchHeaders = ["Content-Range" <:> "0-0/*"] }--    context "with limit/offset parameters" $ do-      it "no parameters return everything" $-        get "/items?select=id&order=id.asc"-          `shouldRespondWith`-          [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "0-14/*"]-          }-      it "top level limit with parameter" $-        get "/items?select=id&order=id.asc&limit=3"-          `shouldRespondWith` [json|[{"id":1},{"id":2},{"id":3}]|]-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "0-2/*"]-          }-      it "headers override get parameters" $-        request methodGet  "/items?select=id&order=id.asc&limit=3"-                     (rangeHdrs $ ByteRangeFromTo 0 1) ""-          `shouldRespondWith` [json|[{"id":1},{"id":2}]|]-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "0-1/*"]-          }--      it "limit works on all levels" $-        get "/clients?select=id,projects(id,tasks(id))&order=id.asc&limit=1&projects.order=id.asc&projects.limit=2&projects.tasks.order=id.asc&projects.tasks.limit=1"-          `shouldRespondWith`-          [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1}]},{"id":2,"tasks":[{"id":3}]}]}]|]-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "0-0/*"]-          }--      it "limit and offset works on first level" $ do-        get "/items?select=id&order=id.asc&limit=3&offset=2"-          `shouldRespondWith` [json|[{"id":3},{"id":4},{"id":5}]|]-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "2-4/*"]-          }-        request methodHead "/items?select=id&order=id.asc&limit=3&offset=2" [] mempty-          `shouldRespondWith` ""-          { matchStatus  = 200-          , matchHeaders = ["Content-Range" <:> "2-4/*"]-          }--      it "succeeds if offset equals 0 as a no-op" $-        get "/items?select=id&offset=0&order=id"-          `shouldRespondWith`-            [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]-            { matchHeaders = ["Content-Range" <:> "0-14/*"] }--      it "succeeds if offset is negative as a no-op" $-        get "/items?select=id&offset=-4&order=id"-          `shouldRespondWith`-            [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]-            { matchHeaders = ["Content-Range" <:> "0-14/*"] }--      it "fails if limit equals 0" $-        get "/items?select=id&limit=0"-          `shouldRespondWith` [json|{"message":"HTTP Range error"}|]-          { matchStatus  = 416-          , matchHeaders = [matchContentTypeJson]-          }--      it "fails if limit is negative" $-        get "/items?select=id&limit=-1"-          `shouldRespondWith` [json|{"message":"HTTP Range error"}|]-          { matchStatus  = 416-          , matchHeaders = [matchContentTypeJson]-          }--    context "when count=planned" $ do-      it "obtains a filtered range" $ do-        request methodGet "/items?select=id&id=gt.8"-            [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            [json|[{"id":9}, {"id":10}, {"id":11}, {"id":12}, {"id":13}, {"id":14}, {"id":15}]|]-            { matchStatus  = 206-            , matchHeaders = ["Content-Range" <:> "0-6/8"]-            }--        request methodGet "/child_entities?select=id&id=gt.3"-            [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            [json|[{"id":4}, {"id":5}, {"id":6}]|]-            { matchStatus  = 206-            , matchHeaders = ["Content-Range" <:> "0-2/4"]-            }--        request methodGet "/getallprojects_view?select=id&id=lt.3"-            [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            [json|[{"id":1}, {"id":2}]|]-            { matchStatus  = 206-            , matchHeaders = ["Content-Range" <:> "0-1/673"]-            }--      it "obtains the full range" $ do-        request methodHead "/items"-            [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            ""-            { matchStatus  = 200-            , matchHeaders = ["Content-Range" <:> "0-14/15"]-            }--        request methodHead "/child_entities"-            [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            ""-            { matchStatus  = 200-            , matchHeaders = ["Content-Range" <:> "0-5/6"]-            }--        request methodHead "/getallprojects_view"-            [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            ""-            { matchStatus  = 206-            , matchHeaders = ["Content-Range" <:> "0-4/2019"]-            }--      it "ignores limit/offset on the planned count" $ do-        request methodHead "/items?limit=2&offset=3"-            [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            ""-            { matchStatus  = 206-            , matchHeaders = ["Content-Range" <:> "3-4/15"]-            }--        request methodHead "/child_entities?limit=2"-           [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            ""-            { matchStatus  = 206-            , matchHeaders = ["Content-Range" <:> "0-1/6"]-            }--        request methodHead "/getallprojects_view?limit=2"-            [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            ""-            { matchStatus  = 206-            , matchHeaders = ["Content-Range" <:> "0-1/2019"]-            }--      it "works with two levels" $-        request methodHead "/child_entities?select=*,entities(*)"-            [("Prefer", "count=planned")]-            ""-          `shouldRespondWith`-            ""-            { matchStatus  = 200-            , matchHeaders = ["Content-Range" <:> "0-5/6"]-            }--    context "with range headers" $ do-      context "of acceptable range" $ do-        it "succeeds with partial content" $ do-          r <- request methodGet  "/items"-                       (rangeHdrs $ ByteRangeFromTo 0 1) ""-          liftIO $ do-            simpleHeaders r `shouldSatisfy`-              matchHeader "Content-Range" "0-1/*"-            simpleStatus r `shouldBe` ok200--        it "understands open-ended ranges" $-          request methodGet "/items"-                  (rangeHdrs $ ByteRangeFrom 0) ""-            `shouldRespondWith` 200--        it "returns an empty body when there are no results" $-          request methodGet "/menagerie"-                  (rangeHdrs $ ByteRangeFromTo 0 1) ""-            `shouldRespondWith` "[]"-            { matchStatus  = 200-            , matchHeaders = ["Content-Range" <:> "*/*"]-            }--        it "allows one-item requests" $ do-          r <- request methodGet  "/items"-                       (rangeHdrs $ ByteRangeFromTo 0 0) ""-          liftIO $ do-            simpleHeaders r `shouldSatisfy`-              matchHeader "Content-Range" "0-0/*"-            simpleStatus r `shouldBe` ok200--        it "handles ranges beyond collection length via truncation" $ do-          r <- request methodGet  "/items"-                       (rangeHdrs $ ByteRangeFromTo 10 100) ""-          liftIO $ do-            simpleHeaders r `shouldSatisfy`-              matchHeader "Content-Range" "10-14/*"-            simpleStatus r `shouldBe` ok200--      context "of invalid range" $ do-        it "fails with 416 for offside range" $-          request methodGet  "/items"-                  (rangeHdrs $ ByteRangeFromTo 1 0) ""-            `shouldRespondWith` 416--        it "refuses a range with nonzero start when there are no items" $-          request methodGet "/menagerie"-                  (rangeHdrsWithCount $ ByteRangeFromTo 1 2) ""-            `shouldRespondWith` "[]"-            { matchStatus  = 416-            , matchHeaders = ["Content-Range" <:> "*/0"]-            }--        it "refuses a range requesting start past last item" $-          request methodGet "/items"-                  (rangeHdrsWithCount $ ByteRangeFromTo 100 199) ""-            `shouldRespondWith` "[]"-            { matchStatus  = 416-            , matchHeaders = ["Content-Range" <:> "*/15"]-            }
− test/Feature/RawOutputTypesSpec.hs
@@ -1,33 +0,0 @@-module Feature.RawOutputTypesSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude-import SpecHelper (acceptHdrs)--spec :: SpecWith ((), Application)-spec = describe "When raw-media-types config variable is missing or left empty" $ do-  let firefoxAcceptHdrs = acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"-      chromeAcceptHdrs = acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"-  it "responds json to a GET request with Firefox Accept headers" $-    request methodGet "/items?id=eq.1" firefoxAcceptHdrs ""-      `shouldRespondWith` [json| [{"id":1}] |]-        { matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }-  it "responds json to a GET request with Chrome Accept headers" $-    request methodGet "/items?id=eq.1" chromeAcceptHdrs ""-      `shouldRespondWith` [json| [{"id":1}] |]-        { matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }--  it "responds json to a GET request to RPC with Firefox Accept headers" $-    request methodGet "/rpc/get_projects_below?id=3" firefoxAcceptHdrs ""-      `shouldRespondWith` [json|[{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}]|]-        { matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }-  it "responds json to a GET request to RPC with Chrome Accept headers" $-    request methodGet "/rpc/get_projects_below?id=3" chromeAcceptHdrs ""-      `shouldRespondWith` [json|[{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}]|]-        { matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }
− test/Feature/RollbackSpec.hs
@@ -1,218 +0,0 @@-module Feature.RollbackSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude hiding (get)---- two helpers functions to make sure that each test can setup and cleanup properly---- creates Item to work with for PATCH and DELETE-postItem =-  request methodPost "/items"-      [("Prefer", "tx=commit"), ("Prefer", "resolution=ignore-duplicates")]-      [json|{"id":0}|]-    `shouldRespondWith`-      ""-      { matchStatus  = 201 }---- removes Items left over from POST, PUT, and PATCH-deleteItems =-  request methodDelete "/items?id=lte.0"-      [("Prefer", "tx=commit")]-      ""-    `shouldRespondWith`-      ""-      { matchStatus  = 204 }--preferDefault  = [("Prefer", "return=representation")]-preferCommit   = [("Prefer", "return=representation"), ("Prefer", "tx=commit")]-preferRollback = [("Prefer", "return=representation"), ("Prefer", "tx=rollback")]--withoutPreferenceApplied      = []-withPreferenceCommitApplied   = [ "Preference-Applied" <:> "tx=commit" ]-withPreferenceRollbackApplied = [ "Preference-Applied" <:> "tx=rollback" ]--shouldRespondToReads reqHeaders respHeaders = do-  it "responds to GET" $ do-    request methodGet "/items?id=eq.1"-        reqHeaders-        ""-      `shouldRespondWith`-        [json|[{"id":1}]|]-        { matchHeaders = respHeaders }--  it "responds to HEAD" $ do-    request methodHead "/items?id=eq.1"-        reqHeaders-        ""-      `shouldRespondWith`-        ""-        { matchHeaders = respHeaders }--  it "responds to GET on RPC" $ do-    request methodGet "/rpc/search?id=1"-        reqHeaders-        ""-      `shouldRespondWith`-        [json|[{"id":1}]|]-        { matchHeaders = respHeaders }--  it "responds to POST on RPC" $ do-    request methodPost "/rpc/search"-        reqHeaders-        [json|{"id":1}|]-      `shouldRespondWith`-        [json|[{"id":1}]|]-        { matchHeaders = respHeaders }--shouldPersistMutations reqHeaders respHeaders = do-  it "does persist post" $ do-    request methodPost "/items"-        reqHeaders-        [json|{"id":0}|]-      `shouldRespondWith`-        [json|[{"id":0}]|]-        { matchStatus  = 201-        , matchHeaders = respHeaders }-    get "/items?id=eq.0"-      `shouldRespondWith`-        [json|[{"id":0}]|]-    deleteItems--  it "does persist put" $ do-    request methodPut "/items?id=eq.0"-        reqHeaders-        [json|{"id":0}|]-      `shouldRespondWith`-        [json|[{"id":0}]|]-        { matchHeaders = respHeaders }-    get "/items?id=eq.0"-      `shouldRespondWith`-        [json|[{"id":0}]|]-    deleteItems--  it "does persist patch" $ do-    postItem-    request methodPatch "/items?id=eq.0"-        reqHeaders-        [json|{"id":-1}|]-      `shouldRespondWith`-        [json|[{"id":-1}]|]-        { matchHeaders = respHeaders }-    get "/items?id=eq.0"-      `shouldRespondWith`-        [json|[]|]-    get "/items?id=eq.-1"-      `shouldRespondWith`-        [json|[{"id":-1}]|]-    deleteItems--  it "does persist delete" $ do-    postItem-    request methodDelete "/items?id=eq.0"-        reqHeaders-        ""-      `shouldRespondWith`-        [json|[{"id":0}]|]-        { matchHeaders = respHeaders }-    get "/items?id=eq.0"-      `shouldRespondWith`-        [json|[]|]--shouldNotPersistMutations reqHeaders respHeaders = do-  it "does not persist post" $ do-    request methodPost "/items"-        reqHeaders-        [json|{"id":0}|]-      `shouldRespondWith`-        [json|[{"id":0}]|]-        { matchStatus  = 201-        , matchHeaders = respHeaders }-    get "/items?id=eq.0"-      `shouldRespondWith`-        [json|[]|]--  it "does not persist put" $ do-    request methodPut "/items?id=eq.0"-        reqHeaders-        [json|{"id":0}|]-      `shouldRespondWith`-        [json|[{"id":0}]|]-        { matchHeaders = respHeaders }-    get "/items?id=eq.0"-      `shouldRespondWith`-        [json|[]|]--  it "does not persist patch" $ do-    request methodPatch "/items?id=eq.1"-        reqHeaders-        [json|{"id":0}|]-      `shouldRespondWith`-        [json|[{"id":0}]|]-        { matchHeaders = respHeaders }-    get "/items?id=eq.0"-      `shouldRespondWith`-        [json|[]|]-    get "items?id=eq.1"-      `shouldRespondWith`-        [json|[{"id":1}]|]--  it "does not persist delete" $ do-    request methodDelete "/items?id=eq.1"-        reqHeaders-        ""-      `shouldRespondWith`-        [json|[{"id":1}]|]-        { matchHeaders = respHeaders }-    get "/items?id=eq.1"-      `shouldRespondWith`-        [json|[{"id":1}]|]--allowed :: SpecWith ((), Application)-allowed = describe "tx-allow-override = true" $ do-  describe "without Prefer tx" $ do-    preferDefault `shouldRespondToReads` withoutPreferenceApplied-    preferDefault `shouldNotPersistMutations` withoutPreferenceApplied--  describe "Prefer tx=commit" $ do-    preferCommit `shouldRespondToReads` withPreferenceCommitApplied-    preferCommit `shouldPersistMutations` withPreferenceCommitApplied--  describe "Prefer tx=rollback" $ do-    preferRollback `shouldRespondToReads` withPreferenceRollbackApplied-    preferRollback `shouldNotPersistMutations` withPreferenceRollbackApplied--disallowed :: SpecWith ((), Application)-disallowed = describe "tx-rollback-all = false, tx-allow-override = false" $ do-  describe "without Prefer tx" $ do-    preferDefault `shouldRespondToReads` withoutPreferenceApplied-    preferDefault `shouldPersistMutations` withoutPreferenceApplied--  describe "Prefer tx=commit" $ do-    preferCommit `shouldRespondToReads` withoutPreferenceApplied-    preferCommit `shouldPersistMutations` withoutPreferenceApplied--  describe "Prefer tx=rollback" $ do-    preferRollback `shouldRespondToReads` withoutPreferenceApplied-    preferRollback `shouldPersistMutations` withoutPreferenceApplied---forced :: SpecWith ((), Application)-forced = describe "tx-rollback-all = true, tx-allow-override = false" $ do-  describe "without Prefer tx" $ do-    preferDefault `shouldRespondToReads` withoutPreferenceApplied-    preferDefault `shouldNotPersistMutations` withoutPreferenceApplied--  describe "Prefer tx=commit" $ do-    preferCommit `shouldRespondToReads` withoutPreferenceApplied-    preferCommit `shouldNotPersistMutations` withoutPreferenceApplied--  describe "Prefer tx=rollback" $ do-    preferRollback `shouldRespondToReads` withoutPreferenceApplied-    preferRollback `shouldNotPersistMutations` withoutPreferenceApplied-
− test/Feature/RootSpec.hs
@@ -1,34 +0,0 @@-module Feature.RootSpec where--import Network.HTTP.Types-import Network.Wai        (Application)--import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude hiding (get)--import SpecHelper--spec :: SpecWith ((), Application)-spec =-  describe "root spec function" $ do-    it "accepts application/openapi+json" $-      request methodGet "/"-        [("Accept","application/openapi+json")] "" `shouldRespondWith`-        [json|{-           "swagger": "2.0",-           "info": {"title": "PostgREST API", "description": "This is a dynamic API generated by PostgREST"}-          }|]-        { matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"] }--    it "accepts application/json" $-      request methodGet "/"-        [("Accept", "application/json")] "" `shouldRespondWith`-        [json| {-            "tableName": "orders_view", "tableSchema": "test",-            "tableDeletable": true, "tableUpdatable": true,-            "tableInsertable": true, "tableDescription": null-          } |]-        { matchHeaders = [matchContentTypeJson] }
− test/Feature/RpcPreRequestGucsSpec.hs
@@ -1,84 +0,0 @@-module Feature.RpcPreRequestGucsSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec          hiding (pendingWith)-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude hiding (get, put)--spec :: SpecWith ((), Application)-spec =-  describe "GUC headers on all methods via pre-request" $ do-    it "succeeds setting the headers on POST" $-      post "/items"-          [json|[{"id": 11111}]|]-        `shouldRespondWith` ""-          { matchStatus = 201-          , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]-          }--    it "succeeds setting the headers on GET and HEAD" $ do-      request methodGet "/items?id=eq.1"-          [("User-Agent", "MSIE 6.0")]-          ""-        `shouldRespondWith`-          [json|[{"id": 1}]|]-          { matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"] }--      request methodHead "/items?id=eq.1"-          [("User-Agent", "MSIE 7.0")]-          ""-        `shouldRespondWith`-          ""-          { matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"] }--      request methodHead "/projects"-          [("Accept", "text/csv")]-          ""-        `shouldRespondWith`-          ""-          { matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"] }--    it "succeeds setting the headers on PATCH" $-        patch "/items?id=eq.1"-            [json|[{"id": 11111}]|]-          `shouldRespondWith` ""-            { matchStatus = 204-            , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]-            }--    it "succeeds setting the headers on PUT" $-      put "/items?id=eq.1"-          [json|[{"id": 1}]|]-        `shouldRespondWith` ""-          { matchStatus = 204-          , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]-          }--    it "succeeds setting the headers on DELETE" $-      delete "/items?id=eq.1"-        `shouldRespondWith`-          ""-          { matchStatus = 204-          , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]-          }-    it "can override the Content-Type header" $ do-      request methodHead "/clients?id=eq.1"-          []-          ""-        `shouldRespondWith`-          ""-          { matchStatus = 200-          , matchHeaders = ["Content-Type" <:> "application/custom+json"]-          }-      request methodHead "/rpc/getallprojects"-          []-          ""-        `shouldRespondWith`-          ""-          { matchStatus = 200-          , matchHeaders = ["Content-Type" <:> "application/custom+json"]-          }
− test/Feature/RpcSpec.hs
@@ -1,1256 +0,0 @@-module Feature.RpcSpec where--import qualified Data.ByteString.Lazy as BL (empty, readFile)--import Network.Wai      (Application)-import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))--import Network.HTTP.Types-import System.IO.Unsafe    (unsafePerformIO)-import Test.Hspec          hiding (pendingWith)-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON-import Text.Heredoc--import PostgREST.Config.PgVersion (PgVersion, pgVersion100,-                                   pgVersion109, pgVersion110,-                                   pgVersion112, pgVersion114,-                                   pgVersion140)--import Protolude  hiding (get)-import SpecHelper--spec :: PgVersion -> SpecWith ((), Application)-spec actualPgVersion =-  describe "remote procedure call" $ do-    context "a proc that returns a set" $ do-      it "returns paginated results" $ do-        request methodPost "/rpc/getitemrange"-                (rangeHdrs (ByteRangeFromTo 0 0))  [json| { "min": 2, "max": 4 } |]-           `shouldRespondWith` [json| [{"id":3}] |]-            { matchStatus = 200-            , matchHeaders = ["Content-Range" <:> "0-0/*"]-            }-        request methodGet "/rpc/getitemrange?min=2&max=4"-                (rangeHdrs (ByteRangeFromTo 0 0)) ""-           `shouldRespondWith` [json| [{"id":3}] |]-            { matchStatus = 200-            , matchHeaders = ["Content-Range" <:> "0-0/*"]-            }-        request methodHead "/rpc/getitemrange?min=2&max=4"-                (rangeHdrs (ByteRangeFromTo 0 0)) ""-           `shouldRespondWith` ""-            { matchStatus = 200-            , matchHeaders = ["Content-Range" <:> "0-0/*"]-            }--      it "includes total count if requested" $ do-        request methodPost "/rpc/getitemrange"-                (rangeHdrsWithCount (ByteRangeFromTo 0 0))-                [json| { "min": 2, "max": 4 } |]-           `shouldRespondWith` [json| [{"id":3}] |]-            { matchStatus = 206 -- it now knows the response is partial-            , matchHeaders = ["Content-Range" <:> "0-0/2"]-            }-        request methodGet "/rpc/getitemrange?min=2&max=4"-                (rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""-           `shouldRespondWith` [json| [{"id":3}] |]-            { matchStatus = 206-            , matchHeaders = ["Content-Range" <:> "0-0/2"]-            }-        request methodHead "/rpc/getitemrange?min=2&max=4"-                (rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""-           `shouldRespondWith` ""-            { matchStatus = 206-            , matchHeaders = ["Content-Range" <:> "0-0/2"]-            }--      it "returns proper json" $ do-        post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`-          [json| [ {"id": 3}, {"id":4} ] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/rpc/getitemrange?min=2&max=4" `shouldRespondWith`-          [json| [ {"id": 3}, {"id":4} ] |]-          { matchHeaders = [matchContentTypeJson] }--      it "returns CSV" $ do-        request methodPost "/rpc/getitemrange"-                (acceptHdrs "text/csv")-                [json| { "min": 2, "max": 4 } |]-           `shouldRespondWith` "id\n3\n4"-            { matchStatus = 200-            , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]-            }-        request methodGet "/rpc/getitemrange?min=2&max=4"-                (acceptHdrs "text/csv") ""-           `shouldRespondWith` "id\n3\n4"-            { matchStatus = 200-            , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]-            }-        request methodHead "/rpc/getitemrange?min=2&max=4"-                (acceptHdrs "text/csv") ""-           `shouldRespondWith` ""-            { matchStatus = 200-            , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]-            }--    context "unknown function" $ do-      it "returns 404" $-        post "/rpc/fakefunc" [json| {} |] `shouldRespondWith` 404--      it "should fail with 404 on unknown proc name" $-        get "/rpc/fake" `shouldRespondWith` 404--      it "should fail with 404 on unknown proc args" $ do-        get "/rpc/sayhello" `shouldRespondWith` 404-        get "/rpc/sayhello?any_arg=value" `shouldRespondWith` 404--      it "should not ignore unknown args and fail with 404" $-        get "/rpc/add_them?a=1&b=2&smthelse=blabla" `shouldRespondWith`-        [json| {-          "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",-          "message":"Could not find the test.add_them(a, b, smthelse) function in the schema cache" } |]-        { matchStatus  = 404-        , matchHeaders = [matchContentTypeJson]-        }--      it "should fail with 404 when no json arg is found with prefer single object" $-        request methodPost "/rpc/sayhello"-          [("Prefer","params=single-object")]-          [json|{}|]-        `shouldRespondWith`-          [json| {-            "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",-            "message":"Could not find the test.sayhello function with a single json or jsonb parameter in the schema cache" } |]-        { matchStatus  = 404-        , matchHeaders = [matchContentTypeJson]-        }--      it "should fail with 404 for overloaded functions with unknown args" $ do-        get "/rpc/overloaded?wrong_arg=value" `shouldRespondWith`-          [json| {-            "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",-            "message":"Could not find the test.overloaded(wrong_arg) function in the schema cache" } |]-          { matchStatus  = 404-          , matchHeaders = [matchContentTypeJson]-          }-        get "/rpc/overloaded?a=1&b=2&wrong_arg=value" `shouldRespondWith`-          [json| {-            "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",-            "message":"Could not find the test.overloaded(a, b, wrong_arg) function in the schema cache" } |]-          { matchStatus  = 404-          , matchHeaders = [matchContentTypeJson]-          }--    context "ambiguous overloaded functions with same parameters' names but different types" $ do-      it "should fail with 300 Multiple Choices without explicit type casts" $-        get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`-          [json| {-            "hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",-            "message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)"}|]-          { matchStatus  = 300-          , matchHeaders = [matchContentTypeJson]-          }--    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`-          [json|[{"id":1,"name":"Windows 7","client_id":1}]|]-        get "/rpc/getproject?id=1" `shouldRespondWith`-          [json|[{"id":1,"name":"Windows 7","client_id":1}]|]--      it "can filter proc results" $ do-        post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" [json| {} |] `shouldRespondWith`-          [json|[{"id":2},{"id":3},{"id":4}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" `shouldRespondWith`-          [json|[{"id":2},{"id":3},{"id":4}]|]-          { matchHeaders = [matchContentTypeJson] }--      it "can limit proc results" $ do-        post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id&limit=2&offset=1" [json| {} |]-          `shouldRespondWith` [json|[{"id":3},{"id":4}]|]-             { matchStatus = 200-             , matchHeaders = ["Content-Range" <:> "1-2/*"] }-        get "/rpc/getallprojects?id=gt.1&id=lt.5&select=id&limit=2&offset=1"-          `shouldRespondWith` [json|[{"id":3},{"id":4}]|]-             { matchStatus = 200-             , matchHeaders = ["Content-Range" <:> "1-2/*"] }--      it "select works on the first level" $ do-        post "/rpc/getproject?select=id,name" [json| { "id": 1} |] `shouldRespondWith`-          [json|[{"id":1,"name":"Windows 7"}]|]-        get "/rpc/getproject?id=1&select=id,name" `shouldRespondWith`-          [json|[{"id":1,"name":"Windows 7"}]|]--    context "foreign entities embedding" $ do-      it "can embed if related tables are in the exposed schema" $ do-        post "/rpc/getproject?select=id,name,client:clients(id),tasks(id)" [json| { "id": 1} |] `shouldRespondWith`-          [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/rpc/getproject?id=1&select=id,name,client:clients(id),tasks(id)" `shouldRespondWith`-          [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]-          { matchHeaders = [matchContentTypeJson] }--      it "cannot embed if the related table is not in the exposed schema" $ do-        post "/rpc/single_article?select=*,article_stars(*)" [json|{ "id": 1}|]-          `shouldRespondWith` 400-        get "/rpc/single_article?id=1&select=*,article_stars(*)"-          `shouldRespondWith` 400--      it "can embed if the related tables are in a hidden schema but exposed as views" $ do-        post "/rpc/single_article?select=id,articleStars(userId)"-            [json|{ "id": 2}|]-          `shouldRespondWith`-            [json|{"id": 2, "articleStars": [{"userId": 3}]}|]-        get "/rpc/single_article?id=2&select=id,articleStars(userId)"-          `shouldRespondWith`-            [json|{"id": 2, "articleStars": [{"userId": 3}]}|]--      it "can embed an M2M relationship table" $-        get "/rpc/getallusers?select=name,tasks(name)&id=gt.1"-          `shouldRespondWith` [json|[-            {"name":"Michael Scott", "tasks":[{"name":"Design IOS"}, {"name":"Code IOS"}, {"name":"Design OSX"}]},-            {"name":"Dwight Schrute","tasks":[{"name":"Design w7"}, {"name":"Design IOS"}]}-          ]|]-          { matchHeaders = [matchContentTypeJson] }--      it "can embed an M2M relationship table that has a parent relationship table" $-        get "/rpc/getallusers?select=name,tasks(name,project:projects(name))&id=gt.1"-          `shouldRespondWith` [json|[-            {"name":"Michael Scott","tasks":[-              {"name":"Design IOS","project":{"name":"IOS"}},-              {"name":"Code IOS","project":{"name":"IOS"}},-              {"name":"Design OSX","project":{"name":"OSX"}}-            ]},-            {"name":"Dwight Schrute","tasks":[-              {"name":"Design w7","project":{"name":"Windows 7"}},-              {"name":"Design IOS","project":{"name":"IOS"}}-            ]}-          ]|]-          { matchHeaders = [matchContentTypeJson] }--      when (actualPgVersion >= pgVersion110) $-        it "can embed if rpc returns domain of table type" $ do-          post "/rpc/getproject_domain?select=id,name,client:clients(id),tasks(id)"-              [json| { "id": 1} |]-            `shouldRespondWith`-              [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]-          get "/rpc/getproject_domain?id=1&select=id,name,client:clients(id),tasks(id)"-            `shouldRespondWith`-              [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]--    context "a proc that returns an empty rowset" $-      it "returns empty json array" $ do-        post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`-          [json| [] |]-          { matchHeaders = [matchContentTypeJson] }-        get "/rpc/test_empty_rowset" `shouldRespondWith`-          [json| [] |]-          { matchHeaders = [matchContentTypeJson] }--    context "proc return types" $ do-      context "returns text" $ do-        it "returns proper json" $-          post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`-            [json|"Hello, world"|]-            { matchHeaders = [matchContentTypeJson] }--        it "can handle unicode" $-          post "/rpc/sayhello" [json| { "name": "¥" } |] `shouldRespondWith`-            [json|"Hello, ¥"|]-            { matchHeaders = [matchContentTypeJson] }--      it "returns array" $-        post "/rpc/ret_array" [json|{}|] `shouldRespondWith`-          [json|[1, 2, 3]|]-          { matchHeaders = [matchContentTypeJson] }--      it "returns setof integers" $-        post "/rpc/ret_setof_integers"-            [json|{}|]-          `shouldRespondWith`-            [json|[1,2,3]|]--      it "returns enum value" $-        post "/rpc/ret_enum" [json|{ "val": "foo" }|] `shouldRespondWith`-          [json|"foo"|]-          { matchHeaders = [matchContentTypeJson] }--      it "returns domain value" $-        post "/rpc/ret_domain" [json|{ "val": "8" }|] `shouldRespondWith`-          [json|8|]-          { matchHeaders = [matchContentTypeJson] }--      it "returns range" $-        post "/rpc/ret_range" [json|{ "low": 10, "up": 20 }|] `shouldRespondWith`-          [json|"[10,20)"|]-          { matchHeaders = [matchContentTypeJson] }--      it "returns row of scalars" $-        post "/rpc/ret_scalars" [json|{}|] `shouldRespondWith`-          [json|[{"a":"scalars", "b":"foo", "c":1, "d":"[10,20)"}]|]-          { matchHeaders = [matchContentTypeJson] }--      it "returns composite type in exposed schema" $-        post "/rpc/ret_point_2d"-            [json|{}|]-          `shouldRespondWith`-            [json|{"x": 10, "y": 5}|]--      it "cannot return composite type in hidden schema" $-        post "/rpc/ret_point_3d" [json|{}|] `shouldRespondWith` 401--      when (actualPgVersion >= pgVersion110) $-        it "returns domain of composite type" $-          post "/rpc/ret_composite_domain"-              [json|{}|]-            `shouldRespondWith`-              [json|{"x": 10, "y": 5}|]--      it "returns single row from table" $-        post "/rpc/single_article?select=id"-            [json|{"id": 2}|]-          `shouldRespondWith`-            [json|{"id": 2}|]--      it "returns null for void" $-        post "/rpc/ret_void"-            [json|{}|]-          `shouldRespondWith`-            "null"-            { matchHeaders = [matchContentTypeJson] }--      it "returns null for an integer with null value" $-        post "/rpc/ret_null"-            [json|{}|]-          `shouldRespondWith`-            "null"-            { matchHeaders = [matchContentTypeJson] }--      context "different types when overloaded" $ do-        it "returns composite type" $-          post "/rpc/ret_point_overloaded"-              [json|{"x": 1, "y": 2}|]-            `shouldRespondWith`-              [json|{"x": 1, "y": 2}|]--        it "returns json scalar with prefer single object" $-          request methodPost "/rpc/ret_point_overloaded" [("Prefer","params=single-object")]-            [json|{"x": 1, "y": 2}|]-            `shouldRespondWith`-            [json|{"x": 1, "y": 2}|]-            { matchHeaders = [matchContentTypeJson] }--    context "proc argument types" $ do-      -- different syntax for array needed for pg<10-      when (actualPgVersion < pgVersion100) $-        it "accepts a variety of arguments (Postgres < 10)" $-          post "/rpc/varied_arguments"-              [json| {-                "double": 3.1,-                "varchar": "hello",-                "boolean": true,-                "date": "20190101",-                "money": 0,-                "enum": "foo",-                "arr": "{a,b,c}",-                "integer": 43,-                "json": {"some key": "some value"},-                "jsonb": {"another key": [1, 2, "3"]}-              } |]-            `shouldRespondWith`-              [json| {-                "double": 3.1,-                "varchar": "hello",-                "boolean": true,-                "date": "2019-01-01",-                "money": "$0.00",-                "enum": "foo",-                "arr": ["a", "b", "c"],-                "integer": 43,-                "json": {"some key": "some value"},-                "jsonb": {"another key": [1, 2, "3"]}-              } |]-              { matchHeaders = [matchContentTypeJson] }--      when (actualPgVersion >= pgVersion100) $-        it "accepts a variety of arguments (Postgres >= 10)" $-          post "/rpc/varied_arguments"-              [json| {-                "double": 3.1,-                "varchar": "hello",-                "boolean": true,-                "date": "20190101",-                "money": 0,-                "enum": "foo",-                "arr": ["a", "b", "c"],-                "integer": 43,-                "json": {"some key": "some value"},-                "jsonb": {"another key": [1, 2, "3"]}-              } |]-            `shouldRespondWith`-              [json| {-                "double": 3.1,-                "varchar": "hello",-                "boolean": true,-                "date": "2019-01-01",-                "money": "$0.00",-                "enum": "foo",-                "arr": ["a", "b", "c"],-                "integer": 43,-                "json": {"some key": "some value"},-                "jsonb": {"another key": [1, 2, "3"]}-              } |]-              { matchHeaders = [matchContentTypeJson] }--      it "accepts a variety of arguments with GET" $-        -- without JSON / JSONB here, because passing those via query string is useless - they just become a "json string" all the time-        get "/rpc/varied_arguments?double=3.1&varchar=hello&boolean=true&date=20190101&money=0&enum=foo&arr=%7Ba,b,c%7D&integer=43"-          `shouldRespondWith`-              [json| {-                "double": 3.1,-                "varchar": "hello",-                "boolean": true,-                "date": "2019-01-01",-                "money": "$0.00",-                "enum": "foo",-                "arr": ["a", "b", "c"],-                "integer": 43,-                "json": {},-                "jsonb": {}-              } |]-            { matchHeaders = [matchContentTypeJson] }--      it "accepts a variety of arguments from an html form" $-        request methodPost "/rpc/varied_arguments"-            [("Content-Type", "application/x-www-form-urlencoded")]-            "double=3.1&varchar=hello&boolean=true&date=20190101&money=0&enum=foo&arr=%7Ba,b,c%7D&integer=43"-          `shouldRespondWith`-              [json| {-                "double": 3.1,-                "varchar": "hello",-                "boolean": true,-                "date": "2019-01-01",-                "money": "$0.00",-                "enum": "foo",-                "arr": ["a", "b", "c"],-                "integer": 43,-                "json": {},-                "jsonb": {}-              } |]-            { matchHeaders = [matchContentTypeJson] }--      it "parses embedded JSON arguments as JSON" $-        post "/rpc/json_argument"-            [json| { "arg": { "key": 3 } } |]-          `shouldRespondWith`-            [json|"object"|]-            { matchHeaders = [matchContentTypeJson] }--      when (actualPgVersion < pgVersion100) $-        it "parses quoted JSON arguments as JSON (Postgres < 10)" $-          post "/rpc/json_argument"-              [json| { "arg": "{ \"key\": 3 }" } |]-            `shouldRespondWith`-              [json|"object"|]-              { matchHeaders = [matchContentTypeJson] }--      when ((actualPgVersion >= pgVersion109 && actualPgVersion < pgVersion110)-            || actualPgVersion >= pgVersion114) $-        it "parses quoted JSON arguments as JSON string (from Postgres 10.9, 11.4)" $-          post "/rpc/json_argument"-              [json| { "arg": "{ \"key\": 3 }" } |]-            `shouldRespondWith`-              [json|"string"|]-              { matchHeaders = [matchContentTypeJson] }--    context "improper input" $ do-      it "rejects unknown content type even if payload is good" $ do-        request methodPost "/rpc/sayhello"-          (acceptHdrs "audio/mpeg3") [json| { "name": "world" } |]-            `shouldRespondWith` 415-        request methodGet "/rpc/sayhello?name=world"-          (acceptHdrs "audio/mpeg3") ""-            `shouldRespondWith` 415-      it "rejects malformed json payload" $ do-        p <- request methodPost "/rpc/sayhello"-          (acceptHdrs "application/json") "sdfsdf"-        liftIO $ do-          simpleStatus p `shouldBe` badRequest400-          isErrorFormat (simpleBody p) `shouldBe` True-      it "treats simple plpgsql raise as invalid input" $ do-        p <- post "/rpc/problem" "{}"-        liftIO $ do-          simpleStatus p `shouldBe` badRequest400-          isErrorFormat (simpleBody p) `shouldBe` True-      it "treats plpgsql assert as internal server error" $ do-        p <- post "/rpc/assert" "{}"-        liftIO $ do-          simpleStatus p `shouldBe` internalServerError500-          isErrorFormat (simpleBody p) `shouldBe` True--    context "unsupported verbs" $ do-      it "DELETE fails" $-        request methodDelete "/rpc/sayhello" [] ""-          `shouldRespondWith`-          [json|{"message":"Bad Request"}|]-          { matchStatus  = 405-          , matchHeaders = [matchContentTypeJson]-          }-      it "PATCH fails" $-        request methodPatch "/rpc/sayhello" [] ""-          `shouldRespondWith` 405-      it "OPTIONS fails" $-        -- TODO: should return info about the function-        request methodOptions "/rpc/sayhello" [] ""-          `shouldRespondWith` 405--    it "executes the proc exactly once per request" $ do-      -- callcounter is persistent even with rollback, because it uses a sequence-      -- reset counter first to make test repeatable-      request methodPost "/rpc/reset_sequence"-          [("Prefer", "tx=commit")]-          [json|{"name": "callcounter_count", "value": 1}|]-        `shouldRespondWith`-          [json|""|]--      -- now the test-      post "/rpc/callcounter"-          [json|{}|]-        `shouldRespondWith`-          [json|1|]--      post "/rpc/callcounter"-          [json|{}|]-        `shouldRespondWith`-          [json|2|]--    context "a proc that receives no parameters" $ do-      it "interprets empty string as empty json object on a post request" $-        post "/rpc/noparamsproc" BL.empty `shouldRespondWith`-          [json| "Return value of no parameters procedure." |]-          { matchHeaders = [matchContentTypeJson] }-      it "interprets empty string as a function with no args on a get request" $-        get "/rpc/noparamsproc" `shouldRespondWith`-          [json| "Return value of no parameters procedure." |]-          { matchHeaders = [matchContentTypeJson] }--    it "returns proper output when having the same return col name as the proc name" $ do-      post "/rpc/test" [json|{}|] `shouldRespondWith`-        [json|[{"test":"hello","value":1}]|] { matchHeaders = [matchContentTypeJson] }-      get "/rpc/test" `shouldRespondWith`-        [json|[{"test":"hello","value":1}]|] { matchHeaders = [matchContentTypeJson] }--    context "procs with OUT/INOUT params" $ do-      it "returns an object result when there is a single OUT param" $ do-        get "/rpc/single_out_param?num=5"-          `shouldRespondWith`-            [json|{"num_plus_one":6}|]--        get "/rpc/single_json_out_param?a=1&b=two"-          `shouldRespondWith`-            [json|{"my_json": {"a": 1, "b": "two"}}|]--      it "returns an object result when there is a single INOUT param" $-        get "/rpc/single_inout_param?num=2"-          `shouldRespondWith`-            [json|{"num":3}|]--      it "returns an object result when there are many OUT params" $-        get "/rpc/many_out_params"-          `shouldRespondWith`-            [json|{"my_json":{"a": 1, "b": "two"},"num":3,"str":"four"}|]--      it "returns an object result when there are many INOUT params" $-        get "/rpc/many_inout_params?num=1&str=two&b=false"-          `shouldRespondWith`-            [json|{"num":1,"str":"two","b":false}|]--    context "procs with TABLE return" $ do-      it "returns an object result when there is a single-column TABLE return type" $-        get "/rpc/single_column_table_return"-          `shouldRespondWith`-            [json|[{"a": "A"}]|]--      it "returns an object result when there is a multi-column TABLE return type" $-        get "/rpc/multi_column_table_return"-          `shouldRespondWith`-            [json|[{"a": "A", "b": "B"}]|]--    context "procs with VARIADIC params" $ do-      when (actualPgVersion < pgVersion100) $-        it "works with POST (Postgres < 10)" $-          post "/rpc/variadic_param"-              [json| { "v": "{hi,hello,there}" } |]-            `shouldRespondWith`-              [json|["hi", "hello", "there"]|]--      when (actualPgVersion >= pgVersion100) $ do-        it "works with POST (Postgres >= 10)" $-          post "/rpc/variadic_param"-              [json| { "v": ["hi", "hello", "there"] } |]-            `shouldRespondWith`-              [json|["hi", "hello", "there"]|]--        context "works with GET and repeated params" $ do-          it "n=0 (through DEFAULT)" $-            get "/rpc/variadic_param"-              `shouldRespondWith`-                [json|[]|]--          it "n=1" $-            get "/rpc/variadic_param?v=hi"-              `shouldRespondWith`-                [json|["hi"]|]--          it "n>1" $-            get "/rpc/variadic_param?v=hi&v=there"-              `shouldRespondWith`-                [json|["hi", "there"]|]--        context "works with POST and repeated params from html form" $ do-          it "n=0 (through DEFAULT)" $-            request methodPost "/rpc/variadic_param"-                [("Content-Type", "application/x-www-form-urlencoded")]-                ""-              `shouldRespondWith`-                [json|[]|]--          it "n=1" $-            request methodPost "/rpc/variadic_param"-                [("Content-Type", "application/x-www-form-urlencoded")]-                "v=hi"-              `shouldRespondWith`-                [json|["hi"]|]--          it "n>1" $-            request methodPost "/rpc/variadic_param"-                [("Content-Type", "application/x-www-form-urlencoded")]-                "v=hi&v=there"-              `shouldRespondWith`-                [json|["hi", "there"]|]--    it "returns last value for repeated params without VARIADIC" $-      get "/rpc/sayhello?name=ignored&name=world"-        `shouldRespondWith`-          [json|"Hello, world"|]--    when (actualPgVersion >= pgVersion100) $-      it "returns last value for repeated non-variadic params in function with other VARIADIC arguments" $-        get "/rpc/sayhello_variadic?name=ignored&name=world&v=unused"-          `shouldRespondWith`-            [json|"Hello, world"|]--    it "can handle procs with args that have a DEFAULT value" $ do-      get "/rpc/many_inout_params?num=1&str=two"-        `shouldRespondWith`-          [json| {"num":1,"str":"two","b":true}|]-      get "/rpc/three_defaults?b=4"-        `shouldRespondWith`-          [json|8|]--    it "can map a RAISE error code and message to a http status" $-      get "/rpc/raise_pt402"-        `shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded" }|]-        { matchStatus  = 402-        , matchHeaders = [matchContentTypeJson]-        }--    it "defaults to status 500 if RAISE code is PT not followed by a number" $-      get "/rpc/raise_bad_pt"-        `shouldRespondWith`-        [json|{"hint": null, "details": null}|]-        { matchStatus  = 500-        , matchHeaders = [ matchContentTypeJson ]-        }--    context "expects a single json object" $ do-      it "does not expand posted json into parameters" $-        request methodPost "/rpc/singlejsonparam"-          [("prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith`-          [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |]-          { matchHeaders = [matchContentTypeJson] }--      it "accepts parameters from an html form" $-        request methodPost "/rpc/singlejsonparam"-          [("Prefer","params=single-object"),("Content-Type", "application/x-www-form-urlencoded")]-          ("integer=7&double=2.71828&varchar=forms+are+fun&" <>-           "boolean=false&date=1900-01-01&money=$3.99&enum=foo") `shouldRespondWith`-          [json| { "integer": "7", "double": "2.71828", "varchar" : "forms are fun"-                 , "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |]-                 { matchHeaders = [matchContentTypeJson] }--      it "works with GET" $-        request methodGet "/rpc/singlejsonparam?p1=1&p2=text" [("Prefer","params=single-object")] ""-          `shouldRespondWith` [json|{ "p1": "1", "p2": "text"}|]-          { matchHeaders = [matchContentTypeJson] }--    context "should work with an overloaded function" $ do-      it "overloaded()" $-        get "/rpc/overloaded"-          `shouldRespondWith`-            [json|[1,2,3]|]--      it "overloaded(json) single-object" $-        request methodPost "/rpc/overloaded"-            [("Prefer","params=single-object")]-            [json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]-          `shouldRespondWith`-            [json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]--      it "overloaded(int, int)" $-        get "/rpc/overloaded?a=1&b=2" `shouldRespondWith` [str|3|]--      it "overloaded(text, text, text)" $-        get "/rpc/overloaded?a=1&b=2&c=3" `shouldRespondWith` [json|"123"|]--      it "overloaded_html_form()" $-        request methodPost "/rpc/overloaded_html_form"-            [("Content-Type", "application/x-www-form-urlencoded")]-            ""-          `shouldRespondWith`-            [json|[1,2,3]|]--      it "overloaded_html_form(json) single-object" $-        request methodPost "/rpc/overloaded_html_form"-            [("Content-Type", "application/x-www-form-urlencoded"), ("Prefer","params=single-object")]-            "a=1&b=2&c=3"-          `shouldRespondWith`-            [json|{"a": "1", "b": "2", "c": "3"}|]--      it "overloaded_html_form(int, int)" $-        request methodPost "/rpc/overloaded_html_form"-            [("Content-Type", "application/x-www-form-urlencoded")]-            "a=1&b=2"-          `shouldRespondWith`-            [str|3|]--      it "overloaded_html_form(text, text, text)" $-        request methodPost "/rpc/overloaded_html_form"-            [("Content-Type", "application/x-www-form-urlencoded")]-            "a=1&b=2&c=3"-          `shouldRespondWith`-            [json|"123"|]--      -- https://github.com/PostgREST/postgrest/issues/1672-      context "embedding overloaded functions with the same signature except for the last param with a default value" $ do-        it "overloaded_default(text default)" $ do-          request methodPost "/rpc/overloaded_default?select=id,name,users(name)"-              [("Content-Type", "application/json")]-              [json|{}|]-            `shouldRespondWith`-              [json|[{"id": 2, "name": "Code w7", "users": [{"name": "Angela Martin"}]}] |]--        it "overloaded_default(int)" $-          request methodPost "/rpc/overloaded_default"-              [("Content-Type", "application/json")]-              [json|{"must_param":1}|]-            `shouldRespondWith`-              [json|{"val":1}|]--        it "overloaded_default(int, text default)" $ do-          request methodPost "/rpc/overloaded_default?select=id,name,users(name)"-              [("Content-Type", "application/json")]-              [json|{"a":4}|]-            `shouldRespondWith`-              [json|[{"id": 5, "name": "Design IOS", "users": [{"name": "Michael Scott"}, {"name": "Dwight Schrute"}]}] |]--        it "overloaded_default(int, int)" $-          request methodPost "/rpc/overloaded_default"-              [("Content-Type", "application/json")]-              [json|{"a":2,"must_param":4}|]-            `shouldRespondWith`-              [json|{"a":2,"val":4}|]--    context "only for POST rpc" $ do-      it "gives a parse filter error if GET style proc args are specified" $-        post "/rpc/sayhello?name=John" [json|{name: "John"}|] `shouldRespondWith` 400--      it "ignores json keys not included in ?columns" $-        post "/rpc/sayhello?columns=name"-          [json|{"name": "John", "smth": "here", "other": "stuff", "fake_id": 13}|]-          `shouldRespondWith`-          [json|"Hello, John"|]-          { matchHeaders = [matchContentTypeJson] }--      it "only takes the first object in case of array of objects payload" $-        post "/rpc/add_them"-          [json|[-            {"a": 1, "b": 2},-            {"a": 4, "b": 6},-            {"a": 100, "b": 200} ]|]-          `shouldRespondWith` "3"-          { matchHeaders = [matchContentTypeJson] }--    context "bulk RPC with params=multiple-objects" $ do-      it "works with a scalar function an returns a json array" $-        request methodPost "/rpc/add_them" [("Prefer", "params=multiple-objects")]-          [json|[-            {"a": 1, "b": 2},-            {"a": 4, "b": 6},-            {"a": 100, "b": 200} ]|]-          `shouldRespondWith`-          [json|-            [3, 10, 300]-          |] { matchHeaders = [matchContentTypeJson] }--      it "works with a scalar function an returns a json array when posting CSV" $-        request methodPost "/rpc/add_them" [("Content-Type", "text/csv"), ("Prefer", "params=multiple-objects")]-          "a,b\n1,2\n4,6\n100,200"-          `shouldRespondWith`-          [json|-            [3, 10, 300]-          |]-          { matchStatus  = 200-          , matchHeaders = [matchContentTypeJson]-          }--      it "works with a non-scalar result" $-        request methodPost "/rpc/get_projects_below?select=id,name" [("Prefer", "params=multiple-objects")]-          [json|[-            {"id": 1},-            {"id": 5} ]|]-          `shouldRespondWith`-          [json|-            [{"id":1,"name":"Windows 7"},-             {"id":2,"name":"Windows 10"},-             {"id":3,"name":"IOS"},-             {"id":4,"name":"OSX"}]-          |] { matchHeaders = [matchContentTypeJson] }--    context "HTTP request env vars" $ do-      it "custom header is set" $-        request methodPost "/rpc/get_guc_value"-                  [("Custom-Header", "test")]-            (-            if actualPgVersion >= pgVersion140 then-              [json| { "prefix": "request.headers", "name": "custom-header" } |]-            else-              [json| { "name": "request.header.custom-header" } |]-            )-            `shouldRespondWith`-            [json|"test"|]-            { matchStatus  = 200-            , matchHeaders = [ matchContentTypeJson ]-            }-      it "standard header is set" $-        request methodPost "/rpc/get_guc_value"-                  [("Origin", "http://example.com")]-            (-            if actualPgVersion >= pgVersion140 then-              [json| { "prefix": "request.headers", "name": "origin" } |]-            else-              [json| { "name": "request.header.origin" } |]-            )-            `shouldRespondWith`-            [json|"http://example.com"|]-            { matchStatus  = 200-            , matchHeaders = [ matchContentTypeJson ]-            }-      it "current role is available as GUC claim" $-        request methodPost "/rpc/get_guc_value" []-            (-            if actualPgVersion >= pgVersion140 then-              [json| { "prefix": "request.jwt.claims", "name": "role" } |]-            else-              [json| { "name": "request.jwt.claim.role" } |]-            )-            `shouldRespondWith`-            [json|"postgrest_test_anonymous"|]-            { matchStatus  = 200-            , matchHeaders = [ matchContentTypeJson ]-            }-      it "single cookie ends up as claims" $-        request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]-          (-          if actualPgVersion >= pgVersion140 then-            [json| {"prefix": "request.cookies", "name":"acookie"} |]-          else-            [json| {"name":"request.cookie.acookie"} |]-          )-            `shouldRespondWith`-            [json|"cookievalue"|]-            { matchStatus = 200-            , matchHeaders = []-            }-      it "multiple cookies ends up as claims" $-        request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]-          (-          if actualPgVersion >= pgVersion140 then-            [json| {"prefix": "request.cookies", "name":"secondcookie"} |]-          else-            [json| {"name":"request.cookie.secondcookie"} |]-          )-            `shouldRespondWith`-            [json|"anothervalue"|]-            { matchStatus = 200-            , matchHeaders = []-            }-      it "app settings available" $-        request methodPost "/rpc/get_guc_value" []-          [json| { "name": "app.settings.app_host" } |]-            `shouldRespondWith`-            [json|"localhost"|]-            { matchStatus  = 200-            , matchHeaders = [ matchContentTypeJson ]-            }-      it "gets the Authorization value" $-        request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]-          (-          if actualPgVersion >= pgVersion140 then-            [json| {"prefix": "request.headers", "name":"authorization"} |]-          else-            [json| {"name":"request.header.authorization"} |]-          )-            `shouldRespondWith`-            [json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]-            { matchStatus = 200-            , matchHeaders = []-            }-      it "gets the http method" $-        request methodPost "/rpc/get_guc_value" []-          [json| {"name":"request.method"} |]-            `shouldRespondWith`-            [json|"POST"|]-            { matchStatus = 200-            , matchHeaders = []-            }-      it "gets the http path" $-        request methodPost "/rpc/get_guc_value" []-          [json| {"name":"request.path"} |]-            `shouldRespondWith`-            [json|"/rpc/get_guc_value"|]-            { matchStatus = 200-            , matchHeaders = []-            }--    context "binary output" $ do-      context "Proc that returns scalar" $ do-        it "can query without selecting column" $-          request methodPost "/rpc/ret_base64_bin" (acceptHdrs "application/octet-stream") ""-            `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"-            { matchStatus = 200-            , matchHeaders = ["Content-Type" <:> "application/octet-stream"]-            }--        it "can get raw output with Accept: text/plain" $-          request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""-            `shouldRespondWith` "Welcome to PostgREST"-            { matchStatus = 200-            , matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]-            }--      context "Proc that returns set of scalars" $-        it "can query without selecting column" $-          request methodGet "/rpc/welcome_twice"-              (acceptHdrs "text/plain")-              ""-            `shouldRespondWith`-              "Welcome to PostgRESTWelcome to PostgREST"-              { matchStatus = 200-              , matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]-              }--      context "Proc that returns rows" $ do-        it "can query if a single column is selected" $-          request methodPost "/rpc/ret_rows_with_base64_bin?select=img" (acceptHdrs "application/octet-stream") ""-            `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="-            { matchStatus = 200-            , matchHeaders = ["Content-Type" <:> "application/octet-stream"]-            }--        it "fails if a single column is not selected" $-          request methodPost "/rpc/ret_rows_with_base64_bin"-              (acceptHdrs "application/octet-stream") ""-            `shouldRespondWith`-              [json| {"message":"application/octet-stream requested but more than one column was selected"} |]-              { matchStatus = 406 }--    context "only for GET rpc" $ do-      it "should fail on mutating procs" $ do-        get "/rpc/callcounter" `shouldRespondWith` 405-        get "/rpc/setprojects?id_l=1&id_h=5&name=FreeBSD" `shouldRespondWith` 405--      it "should filter a proc that has arg name = filter name" $-        get "/rpc/get_projects_below?id=5&id=gt.2&select=id" `shouldRespondWith`-          [json|[{ "id": 3 }, { "id": 4 }]|]-          { matchHeaders = [matchContentTypeJson] }--      it "should work with filters that have the not operator" $ do-        get "/rpc/get_projects_below?id=5&id=not.gt.2&select=id" `shouldRespondWith`-          [json|[{ "id": 1 }, { "id": 2 }]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/rpc/get_projects_below?id=5&id=not.in.(1,3)&select=id" `shouldRespondWith`-          [json|[{ "id": 2 }, { "id": 4 }]|]-          { matchHeaders = [matchContentTypeJson] }--      it "should work with filters that use the plain with language fts operator" $ do-        get "/rpc/get_tsearch?text_search_vector=fts(english).impossible" `shouldRespondWith`-          [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/rpc/get_tsearch?text_search_vector=plfts.impossible" `shouldRespondWith`-          [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]-          { matchHeaders = [matchContentTypeJson] }-        get "/rpc/get_tsearch?text_search_vector=not.fts(english).fun%7Crat" `shouldRespondWith`-          [json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|]-          { matchHeaders = [matchContentTypeJson] }-        when (actualPgVersion >= pgVersion112) $-            get "/rpc/get_tsearch?text_search_vector=wfts.impossible" `shouldRespondWith`-                [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]-                { matchHeaders = [matchContentTypeJson] }--      it "should work with the phraseto_tsquery function" $-        get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`-          [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]-          { 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] }--    context "GUC headers on function calls" $ do-      it "succeeds setting the headers" $ do-        get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id"-          `shouldRespondWith` [json|[{"id": 2}]|]-          {matchHeaders = [-              matchContentTypeJson,-              "X-Test"   <:> "key1=val1; someValue; key2=val2",-              "X-Test-2" <:> "key1=val1"]}-        get "/rpc/get_int_and_guc_headers?num=1"-          `shouldRespondWith` [json|1|]-          {matchHeaders = [-              matchContentTypeJson,-              "X-Test"   <:> "key1=val1; someValue; key2=val2",-              "X-Test-2" <:> "key1=val1"]}-        post "/rpc/get_int_and_guc_headers" [json|{"num": 1}|]-          `shouldRespondWith` [json|1|]-          {matchHeaders = [-              matchContentTypeJson,-              "X-Test"   <:> "key1=val1; someValue; key2=val2",-              "X-Test-2" <:> "key1=val1"]}--      it "fails when setting headers with wrong json structure" $ do-        get "/rpc/bad_guc_headers_1"-          `shouldRespondWith`-          [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]-          { matchStatus  = 500-          , matchHeaders = [ matchContentTypeJson ]-          }-        get "/rpc/bad_guc_headers_2"-          `shouldRespondWith`-          [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]-          { matchStatus  = 500-          , matchHeaders = [ matchContentTypeJson ]-          }-        get "/rpc/bad_guc_headers_3"-          `shouldRespondWith`-          [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]-          { matchStatus  = 500-          , matchHeaders = [ matchContentTypeJson ]-          }-        post "/rpc/bad_guc_headers_1" [json|{}|]-          `shouldRespondWith`-          [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]-          { matchStatus  = 500-          , matchHeaders = [ matchContentTypeJson ]-          }--      it "can set the same http header twice" $-        get "/rpc/set_cookie_twice"-          `shouldRespondWith`-            "null"-            { matchHeaders = [ matchContentTypeJson-                             , "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/"-                             , "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly" ]}--      it "can override the Location header on a trigger" $-        post "/stuff"-            [json|[{"id": 2, "name": "stuff 2"}]|]-          `shouldRespondWith`-            ""-            { matchStatus = 201-            , matchHeaders = ["Location" <:> "/stuff?id=eq.2&overriden=true"]-            }--      -- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535-      -- it was reported that blank headers ` : ` where added and that cause proxies to fail the requests.-      -- These tests are to ensure no blank headers are added.-      context "Blank headers bug" $ do-        it "shouldn't add blank headers on POST" $ do-          r <- request methodPost "/loc_test" [] [json|{"id": "1", "c": "c1"}|]-          liftIO $ do-            let respHeaders = simpleHeaders r-            respHeaders `shouldSatisfy` noBlankHeader--        it "shouldn't add blank headers on PATCH" $ do-          r <- request methodPatch "/loc_test?id=eq.1" [] [json|{"c": "c2"}|]-          liftIO $ do-            let respHeaders = simpleHeaders r-            respHeaders `shouldSatisfy` noBlankHeader--        it "shouldn't add blank headers on GET" $ do-          r <- request methodGet "/loc_test" [] ""-          liftIO $ do-            let respHeaders = simpleHeaders r-            respHeaders `shouldSatisfy` noBlankHeader--        it "shouldn't add blank headers on DELETE" $ do-          r <- request methodDelete "/loc_test?id=eq.1" [] ""-          liftIO $ do-            let respHeaders = simpleHeaders r-            respHeaders `shouldSatisfy` noBlankHeader--      context "GUC status override" $ do-        it "can override the status on RPC" $-          get "/rpc/send_body_status_403"-            `shouldRespondWith`-            [json|{"message" : "invalid user or password"}|]-            { matchStatus  = 403-            , matchHeaders = [ matchContentTypeJson ]-            }--        it "can override the status through trigger" $-          patch "/stuff?id=eq.1"-              [json|[{"name": "updated stuff 1"}]|]-            `shouldRespondWith`-              205--        it "fails when setting invalid status guc" $-          get "/rpc/send_bad_status"-            `shouldRespondWith`-            [json|{"message":"response.status guc must be a valid status code"}|]-            { matchStatus  = 500-            , matchHeaders = [ matchContentTypeJson ]-            }--      context "single unnamed param" $ do-        it "can insert json directly" $-          post "/rpc/unnamed_json_param"-              [json|{"A": 1, "B": 2, "C": 3}|]-            `shouldRespondWith`-              [json|{"A": 1, "B": 2, "C": 3}|]--        it "can insert text directly" $-          request methodPost "/rpc/unnamed_text_param"-            [("Content-Type", "text/plain"), ("Accept", "text/plain")]-            [str|unnamed text arg|]-            `shouldRespondWith`-            [str|unnamed text arg|]--        it "can insert bytea directly" $ do-          let file = unsafePerformIO $ BL.readFile "test/C.png"-          r <- request methodPost "/rpc/unnamed_bytea_param"-            [("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]-            file-          liftIO $ do-            let respBody = simpleBody r-            respBody `shouldBe` file--        it "will err when no function with single unnamed json parameter exists and application/json is specified" $-          request methodPost "/rpc/unnamed_int_param" [("Content-Type", "application/json")]-              [json|{"x": 1, "y": 2}|]-            `shouldRespondWith`-              [json|{-                "hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",-                "message": "Could not find the test.unnamed_int_param(x, y) function or the test.unnamed_int_param function with a single unnamed json or jsonb parameter in the schema cache"-              }|]-              { matchStatus  = 404-              , matchHeaders = [ matchContentTypeJson ]-              }--        it "will err when no function with single unnamed text parameter exists and text/plain is specified" $-          request methodPost "/rpc/unnamed_int_param"-              [("Content-Type", "text/plain")]-              [str|a simple text|]-            `shouldRespondWith`-              [json|{-                "hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",-                "message": "Could not find the test.unnamed_int_param function with a single unnamed text parameter in the schema cache"-              }|]-              { matchStatus  = 404-              , matchHeaders = [ matchContentTypeJson ]-              }--        it "will err when no function with single unnamed bytea parameter exists and application/octet-stream is specified" $-          let file = unsafePerformIO $ BL.readFile "test/C.png" in-          request methodPost "/rpc/unnamed_int_param"-              [("Content-Type", "application/octet-stream")]-              file-          `shouldRespondWith`-            [json|{-              "hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",-              "message": "Could not find the test.unnamed_int_param function with a single unnamed bytea parameter in the schema cache"-            }|]-            { matchStatus  = 404-            , matchHeaders = [ matchContentTypeJson ]-            }--        it "should be able to resolve when a single unnamed json parameter exists and other overloaded functions are found" $ do-          request methodPost "/rpc/overloaded_unnamed_param" [("Content-Type", "application/json")]-              [json|{}|]-            `shouldRespondWith`-              [json| 1 |]-              { matchStatus  = 200-              , matchHeaders = [matchContentTypeJson]-              }-          request methodPost "/rpc/overloaded_unnamed_param" [("Content-Type", "application/json")]-              [json|{"x": 1, "y": 2}|]-            `shouldRespondWith`-              [json| 3 |]-              { matchStatus  = 200-              , matchHeaders = [matchContentTypeJson]-              }--        it "should be able to fallback to the single unnamed parameter function when other overloaded functions are not found" $ do-          request methodPost "/rpc/overloaded_unnamed_param"-              [("Content-Type", "application/json")]-              [json|{"A": 1, "B": 2, "C": 3}|]-            `shouldRespondWith`-              [json|{"A": 1, "B": 2, "C": 3}|]-          request methodPost "/rpc/overloaded_unnamed_param"-              [("Content-Type", "text/plain"), ("Accept", "text/plain")]-              [str|unnamed text arg|]-            `shouldRespondWith`-              [str|unnamed text arg|]-          let file = unsafePerformIO $ BL.readFile "test/C.png"-          r <- request methodPost "/rpc/overloaded_unnamed_param"-            [("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]-            file-          liftIO $ do-            let respBody = simpleBody r-            respBody `shouldBe` file--        it "should fail to fallback to any single unnamed parameter function when using an unsupported Content-Type header" $ do-          request methodPost "/rpc/overloaded_unnamed_param"-              [("Content-Type", "text/csv")]-              "a,b\n1,2\n4,6\n100,200"-            `shouldRespondWith`-              [json| {-                "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",-                "message":"Could not find the test.overloaded_unnamed_param(a, b) function in the schema cache"}|]-              { matchStatus  = 404-              , matchHeaders = [matchContentTypeJson]-              }--        it "should fail with multiple choices when two fallback functions with single unnamed json and jsonb parameters exist" $ do-          request methodPost "/rpc/overloaded_unnamed_json_jsonb_param" [("Content-Type", "application/json")]-              [json|{"A": 1, "B": 2, "C": 3}|]-            `shouldRespondWith`-              [json| {-                "hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",-                "message":"Could not choose the best candidate function between: test.overloaded_unnamed_json_jsonb_param( => json), test.overloaded_unnamed_json_jsonb_param( => jsonb)"}|]-              { matchStatus  = 300-              , matchHeaders = [matchContentTypeJson]-              }
− test/Feature/SingularSpec.hs
@@ -1,324 +0,0 @@-module Feature.SingularSpec where--import Network.Wai      (Application)-import Network.Wai.Test (SResponse (..))--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude  hiding (get)-import SpecHelper---spec :: SpecWith ((), Application)-spec =-  describe "Requesting singular json object" $ do-    let singular = ("Accept", "application/vnd.pgrst.object+json")--    context "with GET request" $ do-      it "fails for zero rows" $-        request methodGet  "/items?id=gt.0&id=lt.0" [singular] ""-          `shouldRespondWith` 406--      it "will select an existing object" $ do-        request methodGet "/items?id=eq.5" [singular] ""-          `shouldRespondWith`-            [json|{"id":5}|]-            { matchHeaders = [matchContentTypeSingular] }-        -- also test without the +json suffix-        request methodGet "/items?id=eq.5"-            [("Accept", "application/vnd.pgrst.object")] ""-          `shouldRespondWith`-            [json|{"id":5}|]-            { matchHeaders = [matchContentTypeSingular] }--      it "can combine multiple prefer values" $-        request methodGet "/items?id=eq.5" [singular, ("Prefer","count=none")] ""-          `shouldRespondWith`-            [json|{"id":5}|]-            { matchHeaders = [matchContentTypeSingular] }--      it "can shape plurality singular object routes" $-        request methodGet "/projects_view?id=eq.1&select=id,name,clients(*),tasks(id,name)" [singular] ""-          `shouldRespondWith`-            [json|{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}|]-            { matchHeaders = [matchContentTypeSingular] }--    context "when updating rows" $ do-      it "works for one row with return=rep" $ do-        request methodPatch "/addresses?id=eq.1"-            [("Prefer", "return=representation"), singular]-            [json| { address: "B Street" } |]-          `shouldRespondWith`-            [json|{"id":1,"address":"B Street"}|]-            { matchHeaders = [matchContentTypeSingular] }--      it "works for one row with return=minimal" $-        request methodPatch "/addresses?id=eq.1"-            [("Prefer", "return=minimal"), singular]-            [json| { address: "C Street" } |]-          `shouldRespondWith`-            ""-            { matchStatus  = 204 }--      it "raises an error for multiple rows" $ do-        request methodPatch "/addresses"-            [("Prefer", "tx=commit"), singular]-            [json| { address: "zzz" } |]-          `shouldRespondWith`-            [json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-            { matchStatus  = 406-            , matchHeaders = [ matchContentTypeSingular-                             , "Preference-Applied" <:> "tx=commit" ]-            }--        -- the rows should not be updated, either-        get "/addresses?id=eq.1"-          `shouldRespondWith`-            [json|[{"id":1,"address":"address 1"}]|]--      it "raises an error for multiple rows with return=rep" $ do-        request methodPatch "/addresses"-            [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]-            [json| { address: "zzz" } |]-          `shouldRespondWith`-            [json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-            { matchStatus  = 406-            , matchHeaders = [ matchContentTypeSingular-                             , "Preference-Applied" <:> "tx=commit" ]-            }--        -- the rows should not be updated, either-        get "/addresses?id=eq.1"-          `shouldRespondWith`-            [json|[{"id":1,"address":"address 1"}]|]--      it "raises an error for zero rows" $-        request methodPatch "/items?id=gt.0&id=lt.0"-                [singular] [json|{"id":1}|]-          `shouldRespondWith`-                  [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-                  { matchStatus  = 406-                  , matchHeaders = [matchContentTypeSingular]-                  }--      it "raises an error for zero rows with return=rep" $-        request methodPatch "/items?id=gt.0&id=lt.0"-                [("Prefer", "return=representation"), singular] [json|{"id":1}|]-          `shouldRespondWith`-                  [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-                  { matchStatus  = 406-                  , matchHeaders = [matchContentTypeSingular]-                  }--    context "when creating rows" $ do-      it "works for one row with return=rep" $ do-        request methodPost "/addresses"-            [("Prefer", "return=representation"), singular]-            [json| [ { id: 102, address: "xxx" } ] |]-          `shouldRespondWith`-            [json|{"id":102,"address":"xxx"}|]-            { matchStatus  = 201-            , matchHeaders = [matchContentTypeSingular]-            }--      it "works for one row with return=minimal" $ do-        request methodPost "/addresses"-            [("Prefer", "return=minimal"), singular]-            [json| [ { id: 103, address: "xxx" } ] |]-          `shouldRespondWith` ""-            { matchStatus  = 201-            , matchHeaders = ["Content-Range" <:> "*/*"]-            }--      it "raises an error when attempting to create multiple entities" $ do-        request methodPost "/addresses"-            [("Prefer", "tx=commit"), singular]-            [json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]-          `shouldRespondWith`-            [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-            { matchStatus  = 406-            , matchHeaders = [ matchContentTypeSingular-                             , "Preference-Applied" <:> "tx=commit" ]-            }--        -- the rows should not exist, either-        get "/addresses?id=eq.200"-          `shouldRespondWith`-            "[]"--      it "raises an error when attempting to create multiple entities with return=rep" $ do-        request methodPost "/addresses"-            [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]-            [json| [ { id: 202, address: "xxx" }, { id: 203, address: "yyy" } ] |]-          `shouldRespondWith`-            [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-            { matchStatus  = 406-            , matchHeaders = [ matchContentTypeSingular-                             , "Preference-Applied" <:> "tx=commit" ]-            }--        -- the rows should not exist, either-        get "/addresses?id=eq.202"-          `shouldRespondWith`-            "[]"--      it "raises an error regardless of return=minimal" $ do-        request methodPost "/addresses"-            [("Prefer", "tx=commit"), ("Prefer", "return=minimal"), singular]-            [json| [ { id: 204, address: "xxx" }, { id: 205, address: "yyy" } ] |]-          `shouldRespondWith`-            [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-            { matchStatus  = 406-            , matchHeaders = [ matchContentTypeSingular-                             , "Preference-Applied" <:> "tx=commit" ]-            }--        -- the rows should not exist, either-        get "/addresses?id=eq.204"-          `shouldRespondWith`-            "[]"--      it "raises an error when creating zero entities" $-        request methodPost "/addresses"-                [singular]-                [json| [ ] |]-          `shouldRespondWith`-                  [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-                  { matchStatus  = 406-                  , matchHeaders = [matchContentTypeSingular]-                  }--      it "raises an error when creating zero entities with return=rep" $-        request methodPost "/addresses"-                [("Prefer", "return=representation"), singular]-                [json| [ ] |]-          `shouldRespondWith`-                  [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-                  { matchStatus  = 406-                  , matchHeaders = [matchContentTypeSingular]-                  }--    context "when deleting rows" $ do-      it "works for one row with return=rep" $ do-        p <- request methodDelete-          "/items?id=eq.11"-          [("Prefer", "return=representation"), singular] ""-        liftIO $ simpleBody p `shouldBe` [json|{"id":11}|]--      it "works for one row with return=minimal" $ do-        p <- request methodDelete-          "/items?id=eq.12"-          [("Prefer", "return=minimal"), singular] ""-        liftIO $ simpleBody p `shouldBe` ""--      it "raises an error when attempting to delete multiple entities" $ do-        request methodDelete "/items?id=gt.0&id=lt.6"-            [("Prefer", "tx=commit"), singular]-            ""-          `shouldRespondWith`-            [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-            { matchStatus  = 406-            , matchHeaders = [ matchContentTypeSingular-                             , "Preference-Applied" <:> "tx=commit" ]-            }--        -- the rows should still exist-        get "/items?id=gt.0&id=lt.6&order=id"-          `shouldRespondWith`-            [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}] |]-            { matchStatus  = 200-            , matchHeaders = ["Content-Range" <:> "0-4/*"]-            }--      it "raises an error when attempting to delete multiple entities with return=rep" $ do-        request methodDelete "/items?id=gt.5&id=lt.11"-            [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular] ""-          `shouldRespondWith`-            [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-            { matchStatus  = 406-            , matchHeaders = [ matchContentTypeSingular-                             , "Preference-Applied" <:> "tx=commit" ]-            }--        -- the rows should still exist-        get "/items?id=gt.5&id=lt.11"-          `shouldRespondWith` [json| [{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |]-            { matchStatus  = 200-            , matchHeaders = ["Content-Range" <:> "0-4/*"]-            }--      it "raises an error when deleting zero entities" $-        request methodDelete "/items?id=lt.0"-                [singular] ""-          `shouldRespondWith`-                [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-                { matchStatus  = 406-                , matchHeaders = [matchContentTypeSingular]-                }--      it "raises an error when deleting zero entities with return=rep" $-        request methodDelete "/items?id=lt.0"-                [("Prefer", "return=representation"), singular] ""-          `shouldRespondWith`-                [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-                { matchStatus  = 406-                , matchHeaders = [matchContentTypeSingular]-                }--    context "when calling a stored proc" $ do-      it "fails for zero rows" $-        request methodPost "/rpc/getproject"-                [singular] [json|{ "id": 9999999}|]-          `shouldRespondWith`-                [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-                { matchStatus  = 406-                , matchHeaders = [matchContentTypeSingular]-                }--      -- this one may be controversial, should vnd.pgrst.object include-      -- the likes of 2 and "hello?"-      it "succeeds for scalar result" $-        request methodPost "/rpc/sayhello"-          [singular] [json|{ "name": "world"}|]-          `shouldRespondWith` 200--      it "returns a single object for json proc" $-        request methodPost "/rpc/getproject"-            [singular] [json|{ "id": 1}|]-          `shouldRespondWith`-            [json|{"id":1,"name":"Windows 7","client_id":1}|]-            { matchHeaders = [matchContentTypeSingular] }--      it "fails for multiple rows" $-        request methodPost "/rpc/getallprojects"-                [singular] "{}"-          `shouldRespondWith`-                [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-                { matchStatus  = 406-                , matchHeaders = [matchContentTypeSingular]-                }--      it "fails for multiple rows with rolled back changes" $ do-        post "/rpc/getproject?select=id,name"-            [json| {"id": 1} |]-          `shouldRespondWith`-            [json|[{"id":1,"name":"Windows 7"}]|]--        request methodPost "/rpc/setprojects"-            [("Prefer", "tx=commit"), singular]-            [json| {"id_l": 1, "id_h": 2, "name": "changed"} |]-          `shouldRespondWith`-            [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]-            { matchStatus  = 406-            , matchHeaders = [ matchContentTypeSingular-                             , "Preference-Applied" <:> "tx=commit" ]-            }--        -- should rollback function-        post "/rpc/getproject?select=id,name"-            [json| {"id": 1} |]-          `shouldRespondWith`-            [json|[{"id":1,"name":"Windows 7"}]|]
− test/Feature/UnicodeSpec.hs
@@ -1,34 +0,0 @@-module Feature.UnicodeSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude hiding (get)--spec :: SpecWith ((), Application)-spec =-  describe "Reading and writing to unicode schema and table names" $-    it "Can read and write values" $ do-      get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"-        `shouldRespondWith` "[]"--      request methodPost "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"-          [("Prefer", "tx=commit"), ("Prefer", "return=representation")]-          [json| { "هویت": 1 } |]-        `shouldRespondWith`-          [json| [{ "هویت": 1 }] |]-          { matchStatus = 201 }--      get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"-        `shouldRespondWith`-          [json| [{ "هویت": 1 }] |]--      request methodDelete "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"-          [("Prefer", "tx=commit")]-          ""-        `shouldRespondWith`-          204
− test/Feature/UpdateSpec.hs
@@ -1,349 +0,0 @@-module Feature.UpdateSpec where--import Network.Wai (Application)-import Test.Hspec  hiding (pendingWith)--import Network.HTTP.Types-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import Protolude  hiding (get)-import SpecHelper--spec :: SpecWith ((), Application)-spec = do-  describe "Patching record" $ do-    context "to unknown uri" $-      it "indicates no table found by returning 404" $-        request methodPatch "/fake" []-          [json| { "real": false } |]-            `shouldRespondWith` 404--    context "on an empty table" $-      it "indicates no records found to update by returning 404" $-        request methodPatch "/empty_table" []-          [json| { "extra":20 } |]-          `shouldRespondWith` ""-          { matchStatus  = 404,-            matchHeaders = []-          }--    context "with invalid json payload" $-      it "fails with 400 and error" $-        request methodPatch "/simple_pk" [] "}{ x = 2"-          `shouldRespondWith`-          [json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]-          { matchStatus  = 400,-            matchHeaders = [matchContentTypeJson]-          }--    context "with no payload" $-      it "fails with 400 and error" $-        request methodPatch "/items" [] ""-          `shouldRespondWith`-          [json|{"message":"Error in $: not enough input"}|]-          { matchStatus  = 400,-            matchHeaders = [matchContentTypeJson]-          }--    context "in a nonempty table" $ do-      it "can update a single item" $ do-        patch "/items?id=eq.2"-            [json| { "id":42 } |]-          `shouldRespondWith`-            ""-            { matchStatus  = 204-            , matchHeaders = ["Content-Range" <:> "0-0/*"]-            }--      it "returns empty array when no rows updated and return=rep" $-        request methodPatch "/items?id=eq.999999"-          [("Prefer", "return=representation")] [json| { "id":999999 } |]-          `shouldRespondWith` "[]"-          {-            matchStatus  = 404,-            matchHeaders = []-          }--      it "gives a 404 when no rows updated" $-        request methodPatch "/items?id=eq.99999999" []-          [json| { "id": 42 } |]-            `shouldRespondWith` 404--      it "returns updated object as array when return=rep" $-        request methodPatch "/items?id=eq.2"-          [("Prefer", "return=representation")] [json| { "id":2 } |]-          `shouldRespondWith` [json|[{"id":2}]|]-          { matchStatus  = 200,-            matchHeaders = ["Content-Range" <:> "0-0/*"]-          }--      it "can update multiple items" $ do-        get "/no_pk?select=a&b=eq.1"-          `shouldRespondWith`-            [json|[]|]--        request methodPatch "/no_pk?b=eq.0"-            [("Prefer", "tx=commit")]-            [json| { b: "1" } |]-          `shouldRespondWith`-            ""-            { matchStatus  = 204-            , matchHeaders = ["Content-Range" <:> "0-1/*"-                             , "Preference-Applied" <:> "tx=commit" ]-            }--        -- check it really got updated-        get "/no_pk?select=a&b=eq.1"-          `shouldRespondWith`-            [json|[ { a: "1" }, { a: "2" } ]|]--        -- put value back for other tests-        request methodPatch "/no_pk?b=eq.1"-            [("Prefer", "tx=commit")]-            [json| { b: "0" } |]-          `shouldRespondWith`-            204--      it "can set a column to NULL" $ do-        request methodPatch "/no_pk?a=eq.1"-            [("Prefer", "return=representation")]-            [json| { b: null } |]-          `shouldRespondWith`-            [json| [{ a: "1", b: null }] |]--      context "filtering by a computed column" $ do-        it "is successful" $-          request methodPatch-            "/items?is_first=eq.true"-            [("Prefer", "return=representation")]-            [json| { id: 100 } |]-            `shouldRespondWith` [json| [{ id: 100 }] |]-            { matchStatus  = 200,-              matchHeaders = [matchContentTypeJson, "Content-Range" <:> "0-0/*"]-            }--        it "indicates no records updated by returning 404" $-          request methodPatch-            "/items?always_true=eq.false"-            [("Prefer", "return=representation")]-            [json| { id: 100 } |]-            `shouldRespondWith` "[]"-            { matchStatus  = 404,-              matchHeaders = []-            }--      context "with representation requested" $ do-        it "can provide a representation" $ do-          _ <- post "/items"-            [json| { id: 1 } |]-          request methodPatch-            "/items?id=eq.1"-            [("Prefer", "return=representation")]-            [json| { id: 99 } |]-            `shouldRespondWith` [json| [{id:99}] |]-            { matchHeaders = [matchContentTypeJson] }-          -- put value back for other tests-          void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]--        it "can return computed columns" $-          request methodPatch-            "/items?id=eq.1&select=id,always_true"-            [("Prefer", "return=representation")]-            [json| { id: 1 } |]-            `shouldRespondWith` [json| [{ id: 1, always_true: true }] |]-            { matchHeaders = [matchContentTypeJson] }--        it "can select overloaded computed columns" $ do-          request methodPatch-            "/items?id=eq.1&select=id,computed_overload"-            [("Prefer", "return=representation")]-            [json| { id: 1 } |]-            `shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]-            { matchHeaders = [matchContentTypeJson] }-          request methodPatch-            "/items2?id=eq.1&select=id,computed_overload"-            [("Prefer", "return=representation")]-            [json| { id: 1 } |]-            `shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]-            { matchHeaders = [matchContentTypeJson] }--      it "ignores ?select= when return not set or return=minimal" $ do-        request methodPatch "/items?id=eq.1&select=id" [] [json| { id:1 } |]-          `shouldRespondWith` ""-          {-            matchStatus  = 204,-            matchHeaders = ["Content-Range" <:> "0-0/*"]-          }-        request methodPatch "/items?id=eq.1&select=id" [("Prefer", "return=minimal")] [json| { id:1 } |]-          `shouldRespondWith` ""-          {-            matchStatus  = 204,-            matchHeaders = ["Content-Range" <:> "0-0/*"]-          }--      context "when patching with an empty body" $ do-        it "makes no updates and returns 204 without return= and without ?select=" $ do-          request methodPatch "/items" [] [json| {} |]-            `shouldRespondWith` ""-            {-              matchStatus  = 204,-              matchHeaders = ["Content-Range" <:> "*/*"]-            }--          request methodPatch "/items" [] [json| [] |]-            `shouldRespondWith` ""-            {-              matchStatus  = 204,-              matchHeaders = ["Content-Range" <:> "*/*"]-            }--          request methodPatch "/items" [] [json| [{}] |]-            `shouldRespondWith` ""-            {-              matchStatus  = 204,-              matchHeaders = ["Content-Range" <:> "*/*"]-            }--        it "makes no updates and returns 204 without return= and with ?select=" $ do-          request methodPatch "/items?select=id" [] [json| {} |]-            `shouldRespondWith` ""-            {-              matchStatus  = 204,-              matchHeaders = ["Content-Range" <:> "*/*"]-            }--          request methodPatch "/items?select=id" [] [json| [] |]-            `shouldRespondWith` ""-            {-              matchStatus  = 204,-              matchHeaders = ["Content-Range" <:> "*/*"]-            }--          request methodPatch "/items?select=id" [] [json| [{}] |]-            `shouldRespondWith` ""-            {-              matchStatus  = 204,-              matchHeaders = ["Content-Range" <:> "*/*"]-            }--        it "makes no updates and returns 200 with return=rep and without ?select=" $-          request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |]-            `shouldRespondWith` "[]"-            {-              matchStatus  = 200,-              matchHeaders = ["Content-Range" <:> "*/*"]-            }--        it "makes no updates and returns 200 with return=rep and with ?select=" $-          request methodPatch "/items?select=id" [("Prefer", "return=representation")] [json| {} |]-            `shouldRespondWith` "[]"-            {-              matchStatus  = 200,-              matchHeaders = ["Content-Range" <:> "*/*"]-            }--        it "makes no updates and returns 200 with return=rep and with ?select= for overloaded computed columns" $-          request methodPatch "/items?select=id,computed_overload" [("Prefer", "return=representation")] [json| {} |]-            `shouldRespondWith` "[]"-            {-              matchStatus  = 200,-              matchHeaders = ["Content-Range" <:> "*/*"]-            }--    context "with unicode values" $-      it "succeeds and returns values intact" $ do-        request methodPatch "/no_pk?a=eq.1"-            [("Prefer", "return=representation")]-            [json| { "a":"圍棋", "b":"¥" } |]-          `shouldRespondWith`-            [json|[ { "a":"圍棋", "b":"¥" } ]|]--    context "PATCH with ?columns parameter" $ do-      it "ignores json keys not included in ?columns" $ do-        request methodPatch "/articles?id=eq.1&columns=body"-            [("Prefer", "return=representation")]-            [json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |]-          `shouldRespondWith`-            [json|[{"id": 1, "body": "Some real content", "owner": "postgrest_test_anonymous"}]|]--      it "ignores json keys and gives 404 if no record updated" $-        request methodPatch "/articles?id=eq.2001&columns=body" [("Prefer", "return=representation")]-          [json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith` 404--  context "tables with self reference foreign keys" $ do-    it "embeds children after update" $-      request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)"-              [("Prefer", "return=representation")]-        [json|{"name": "tardis-patched"}|]-        `shouldRespondWith`-        [json|-          [ { "id": 0, "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]-        |]-        { matchStatus  = 200,-          matchHeaders = [matchContentTypeJson]-        }--    it "embeds parent, children and grandchildren after update" $-      request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)"-              [("Prefer", "return=representation")]-        [json|{"name": "tardis-patched-2"}|]-        `shouldRespondWith`-        [json| [-          {-            "id": 0,-            "name": "tardis-patched-2",-            "parent_content": { "name": "wat" },-            "web_content": [-                { "name": "fezz", "web_content": [ { "name": "wut" } ] },-                { "name": "foo",  "web_content": [] },-                { "name": "bar",  "web_content": [] }-            ]-          }-        ] |]-        { matchStatus  = 200,-          matchHeaders = [matchContentTypeJson]-        }--    it "embeds children after update without explicitly including the id in the ?select" $-      request methodPatch "/web_content?id=eq.0&select=name,web_content(name)"-              [("Prefer", "return=representation")]-        [json|{"name": "tardis-patched"}|]-        `shouldRespondWith`-        [json|-          [ { "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]-        |]-        { matchStatus  = 200,-          matchHeaders = [matchContentTypeJson]-        }--    it "embeds an M2M relationship plus parent after update" $-      request methodPatch "/users?id=eq.1&select=name,tasks(name,project:projects(name))"-              [("Prefer", "return=representation")]-        [json|{"name": "Kevin Malone"}|]-        `shouldRespondWith`-        [json|[-          {-            "name": "Kevin Malone",-            "tasks": [-                { "name": "Design w7", "project": { "name": "Windows 7" } },-                { "name": "Code w7", "project": { "name": "Windows 7" } },-                { "name": "Design w10", "project": { "name": "Windows 10" } },-                { "name": "Code w10", "project": { "name": "Windows 10" } }-            ]-          }-        ]|]-        { matchStatus  = 200,-          matchHeaders = [matchContentTypeJson]-        }--  context "table with limited privileges" $ do-    it "succeeds updating row and gives a 204 when using return=minimal" $-      request methodPatch "/app_users?id=eq.1" [("Prefer", "return=minimal")]-        [json| { "password": "passxyz" } |]-          `shouldRespondWith` 204--    it "can update without return=minimal and no explicit select" $-      request methodPatch "/app_users?id=eq.1" []-        [json| { "password": "passabc" } |]-          `shouldRespondWith` 204
− test/Feature/UpsertSpec.hs
@@ -1,427 +0,0 @@-module Feature.UpsertSpec where--import Network.Wai (Application)--import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Test.Hspec.Wai.JSON--import PostgREST.Config.PgVersion (PgVersion, pgVersion110)--import Protolude  hiding (get, put)-import SpecHelper--spec :: PgVersion -> SpecWith ((), Application)-spec actualPgVersion =-  describe "UPSERT" $ do-    context "with POST" $ do-      context "when Prefer: resolution=merge-duplicates is specified" $ do-        it "INSERTs and UPDATEs rows on pk conflict" $-          request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]-            [json| [-              { "name": "Javascript", "rank": 6 },-              { "name": "Java", "rank": 2 },-              { "name": "C", "rank": 1 }-            ]|] `shouldRespondWith` [json| [-              { "name": "Javascript", "rank": 6 },-              { "name": "Java", "rank": 2 },-              { "name": "C", "rank": 1 }-            ]|]-            { matchStatus = 201-            , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]-            }--        it "INSERTs and UPDATEs row on composite pk conflict" $-          request methodPost "/employees" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]-            [json| [-              { "first_name": "Frances M.", "last_name": "Roe", "salary": "30000" },-              { "first_name": "Peter S.", "last_name": "Yang", "salary": 42000 }-            ]|] `shouldRespondWith` [json| [-              { "first_name": "Frances M.", "last_name": "Roe", "salary": "$30,000.00", "company": "One-Up Realty", "occupation": "Author" },-              { "first_name": "Peter S.", "last_name": "Yang", "salary": "$42,000.00", "company": null, "occupation": null }-            ]|]-            { matchStatus = 201-            , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]-            }--        when (actualPgVersion >= pgVersion110) $-          it "INSERTs and UPDATEs rows on composite pk conflict for partitioned tables" $-            request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]-              [json| [-                { "name": "Murcielago", "year": 2001, "car_brand_name": null},-                { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" }-              ]|] `shouldRespondWith` [json| [-                { "name": "Murcielago", "year": 2001, "car_brand_name": null},-                { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" }-              ]|]-              { matchStatus = 201-              , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]-              }--        it "succeeds when the payload has no elements" $-          request methodPost "/articles" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]-            [json|[]|] `shouldRespondWith`-            [json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }--        it "INSERTs and UPDATEs rows on single unique key conflict" $-          request methodPost "/single_unique?on_conflict=unique_key" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]-            [json| [-              { "unique_key": 1, "value": "B" },-              { "unique_key": 2, "value": "C" }-            ]|] `shouldRespondWith` [json| [-              { "unique_key": 1, "value": "B" },-              { "unique_key": 2, "value": "C" }-            ]|]-            { matchStatus = 201-            , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]-            }--        it "INSERTs and UPDATEs rows on compound unique keys conflict" $-          request methodPost "/compound_unique?on_conflict=key1,key2" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]-            [json| [-              { "key1": 1, "key2": 1, "value": "B" },-              { "key1": 1, "key2": 2, "value": "C" }-            ]|] `shouldRespondWith` [json| [-              { "key1": 1, "key2": 1, "value": "B" },-              { "key1": 1, "key2": 2, "value": "C" }-            ]|]-            { matchStatus = 201-            , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]-            }--      context "when Prefer: resolution=ignore-duplicates is specified" $ do-        it "INSERTs and ignores rows on pk conflict" $-          request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]-            [json|[-              { "name": "PHP", "rank": 9 },-              { "name": "Python", "rank": 10 }-            ]|] `shouldRespondWith` [json|[-              { "name": "PHP", "rank": 9 }-            ]|]-            { matchStatus = 201-            , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]-            }--        it "INSERTs and ignores rows on composite pk conflict" $-          request methodPost "/employees" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]-            [json|[-              { "first_name": "Daniel B.", "last_name": "Lyon", "salary": "72000", "company": null, "occupation": null },-              { "first_name": "Sara M.", "last_name": "Torpey", "salary": 60000, "company": "Burstein-Applebee", "occupation": "Soil scientist" }-            ]|] `shouldRespondWith` [json|[-              { "first_name": "Sara M.", "last_name": "Torpey", "salary": "$60,000.00", "company": "Burstein-Applebee", "occupation": "Soil scientist" }-            ]|]-            { matchStatus = 201-            , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]-            }--        when (actualPgVersion >= pgVersion110) $-          it "INSERTs and ignores rows on composite pk conflict for partitioned tables" $-            request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]-              [json| [-                { "name": "Murcielago", "year": 2001, "car_brand_name": "Ferrari" },-                { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" }-              ]|] `shouldRespondWith` [json| [-                { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" }-              ]|]-              { matchStatus = 201-              , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]-              }--        it "INSERTs and ignores rows on single unique key conflict" $-          request methodPost "/single_unique?on_conflict=unique_key"-              [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]-              [json| [-                { "unique_key": 1, "value": "B" },-                { "unique_key": 2, "value": "C" },-                { "unique_key": 3, "value": "D" }-              ]|]-            `shouldRespondWith`-              [json| [-                { "unique_key": 2, "value": "C" },-                { "unique_key": 3, "value": "D" }-              ]|]-              { matchStatus = 201-              , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates"]-              }--        it "INSERTs and UPDATEs rows on compound unique keys conflict" $-          request methodPost "/compound_unique?on_conflict=key1,key2"-              [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]-              [json| [-                { "key1": 1, "key2": 1, "value": "B" },-                { "key1": 1, "key2": 2, "value": "C" },-                { "key1": 1, "key2": 3, "value": "D" }-              ]|]-            `shouldRespondWith`-              [json| [-                { "key1": 1, "key2": 2, "value": "C" },-                { "key1": 1, "key2": 3, "value": "D" }-              ]|]-              { matchStatus = 201-              , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates"]-              }--      it "succeeds if the table has only PK cols and no other cols" $ do-        request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]-          [json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]-          `shouldRespondWith`-          [json|[ { "id": 3} ]|]-          { matchStatus = 201 ,-            matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates",-            matchContentTypeJson] }--        request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]-          [json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]-          `shouldRespondWith`-          [json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]-          { matchStatus = 201 ,-            matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates",-            matchContentTypeJson] }--      it "succeeds and ignores the Prefer: resolution header(no Preference-Applied present) if the table has no PK" $-        request methodPost "/no_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]-          [json|[ { "a": "1", "b": "0" } ]|]-          `shouldRespondWith`-          [json|[ { "a": "1", "b": "0" } ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }--      it "succeeds if not a single resource is created" $ do-        request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]-          [json|[ { "name": "Java", "rank": 1 } ]|] `shouldRespondWith`-          [json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }-        request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]-          [json|[ { "name": "Java", "rank": 1 }, { "name": "C", "rank": 2 } ]|] `shouldRespondWith`-          [json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }--    context "with PUT" $ do-      context "Restrictions" $ do-        it "fails if Range is specified" $-          request methodPut "/tiobe_pls?name=eq.Javascript" [("Range", "0-5")]-            [json| [ { "name": "Javascript", "rank": 1 } ]|]-            `shouldRespondWith`-            [json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]-            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }--        it "fails if limit is specified" $-          put "/tiobe_pls?name=eq.Javascript&limit=1"-            [json| [ { "name": "Javascript", "rank": 1 } ]|]-            `shouldRespondWith`-            [json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]-            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }--        it "fails if offset is specified" $-          put "/tiobe_pls?name=eq.Javascript&offset=1"-            [json| [ { "name": "Javascript", "rank": 1 } ]|]-            `shouldRespondWith`-            [json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]-            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }--        it "rejects every other filter than pk cols eq's" $ do-          put "/tiobe_pls?rank=eq.19"-            [json| [ { "name": "Go", "rank": 19 } ]|]-            `shouldRespondWith`-            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]-            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }--          put "/tiobe_pls?id=not.eq.Java"-            [json| [ { "name": "Go", "rank": 19 } ]|]-            `shouldRespondWith`-            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]-            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }-          put "/tiobe_pls?id=in.(Go)"-            [json| [ { "name": "Go", "rank": 19 } ]|]-            `shouldRespondWith`-            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]-            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }-          put "/tiobe_pls?and=(id.eq.Go)"-            [json| [ { "name": "Go", "rank": 19 } ]|]-            `shouldRespondWith`-            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]-            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }--        it "fails if not all composite key cols are specified as eq filters" $ do-          put "/employees?first_name=eq.Susan"-            [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]-            `shouldRespondWith`-            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]-            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }-          put "/employees?last_name=eq.Heidt"-            [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]-            `shouldRespondWith`-            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]-            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }--      it "fails if the uri primary key doesn't match the payload primary key" $ do-        put "/tiobe_pls?name=eq.MATLAB" [json| [ { "name": "Perl", "rank": 17 } ]|]-          `shouldRespondWith`-          [json|{"message":"Payload values do not match URL in primary key column(s)"}|]-          { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }-        put "/employees?first_name=eq.Wendy&last_name=eq.Anderson"-          [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]-          `shouldRespondWith`-          [json|{"message":"Payload values do not match URL in primary key column(s)"}|]-          { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }--      it "fails if the table has no PK" $-        put "/no_pk?a=eq.one&b=eq.two" [json| [ { "a": "one", "b": "two" } ]|]-          `shouldRespondWith`-          [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]-          { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }--      context "Inserting row" $ do-        it "succeeds on table with single pk col" $ do-          -- assert that the next request will indeed be an insert-          get "/tiobe_pls?name=eq.Go"-            `shouldRespondWith`-              [json|[]|]--          request methodPut "/tiobe_pls?name=eq.Go"-              [("Prefer", "return=representation")]-              [json| [ { "name": "Go", "rank": 19 } ]|]-            `shouldRespondWith`-              [json| [ { "name": "Go", "rank": 19 } ]|]--        it "succeeds on table with composite pk" $ do-          -- assert that the next request will indeed be an insert-          get "/employees?first_name=eq.Susan&last_name=eq.Heidt"-            `shouldRespondWith`-              [json|[]|]--          request methodPut "/employees?first_name=eq.Susan&last_name=eq.Heidt"-              [("Prefer", "return=representation")]-              [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]-            `shouldRespondWith`-              [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]--        when (actualPgVersion >= pgVersion110) $-          it "succeeds on a partitioned table with composite pk" $ do-            -- assert that the next request will indeed be an insert-            get "/car_models?name=eq.Supra&year=eq.2021"-              `shouldRespondWith`-                [json|[]|]--            request methodPut "/car_models?name=eq.Supra&year=eq.2021"-                [("Prefer", "return=representation")]-                [json| [ { "name": "Supra", "year": 2021 } ]|]-              `shouldRespondWith`-                [json| [ { "name": "Supra", "year": 2021, "car_brand_name": null } ]|]--        it "succeeds if the table has only PK cols and no other cols" $ do-          -- assert that the next request will indeed be an insert-          get "/only_pk?id=eq.10"-            `shouldRespondWith`-              [json|[]|]--          request methodPut "/only_pk?id=eq.10"-              [("Prefer", "return=representation")]-              [json|[ { "id": 10 } ]|]-            `shouldRespondWith`-              [json|[ { "id": 10 } ]|]--      context "Updating row" $ do-        it "succeeds on table with single pk col" $ do-          -- assert that the next request will indeed be an update-          get "/tiobe_pls?name=eq.Java"-            `shouldRespondWith`-              [json|[ { "name": "Java", "rank": 1 } ]|]--          request methodPut "/tiobe_pls?name=eq.Java"-              [("Prefer", "return=representation")]-              [json| [ { "name": "Java", "rank": 13 } ]|]-            `shouldRespondWith`-              [json| [ { "name": "Java", "rank": 13 } ]|]--        -- TODO: move this to SingularSpec?-        it "succeeds if the payload has more than one row, but it only puts the first element" $ do-          -- assert that the next request will indeed be an update-          get "/tiobe_pls?name=eq.Java"-            `shouldRespondWith`-              [json|[ { "name": "Java", "rank": 1 } ]|]--          request methodPut "/tiobe_pls?name=eq.Java"-              [("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]-              [json| [ { "name": "Java", "rank": 19 }, { "name": "Swift", "rank": 12 } ] |]-            `shouldRespondWith`-              [json|{ "name": "Java", "rank": 19 }|]-              { matchHeaders = [matchContentTypeSingular] }--        it "succeeds on table with composite pk" $ do-          -- assert that the next request will indeed be an update-          get "/employees?first_name=eq.Frances M.&last_name=eq.Roe"-            `shouldRespondWith`-              [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$24,000.00", "company": "One-Up Realty", "occupation": "Author" } ]|]--          request methodPut "/employees?first_name=eq.Frances M.&last_name=eq.Roe"-              [("Prefer", "return=representation")]-              [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "60000", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]-            `shouldRespondWith`-              [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]--        when (actualPgVersion >= pgVersion110) $-          it "succeeds on a partitioned table with composite pk" $ do-            -- assert that the next request will indeed be an update-            get "/car_models?name=eq.DeLorean&year=eq.1981"-              `shouldRespondWith`-                [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": "DMC" } ]|]--            request methodPut "/car_models?name=eq.DeLorean&year=eq.1981"-                [("Prefer", "return=representation")]-                [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|]-              `shouldRespondWith`-                [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|]--        it "succeeds if the table has only PK cols and no other cols" $ do-          -- assert that the next request will indeed be an update-          get "/only_pk?id=eq.1"-            `shouldRespondWith`-              [json|[ { "id": 1 } ]|]--          request methodPut "/only_pk?id=eq.1"-              [("Prefer", "return=representation")]-              [json|[ { "id": 1 } ]|]-            `shouldRespondWith`-              [json|[ { "id": 1 } ]|]--      -- TODO: move this to SingularSpec?-      it "works with return=representation and vnd.pgrst.object+json" $-        request methodPut "/tiobe_pls?name=eq.Ruby"-          [("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]-          [json| [ { "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" $ 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"]-            }--      it "works with POST and ignore-duplicates headers" $ do-        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|[-              { "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }-            ]|]-            { matchStatus = 201-            , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates"]-            }--      it "works with PUT" $ do-        put "/UnitTest?idUnitTest=eq.1" [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] `shouldRespondWith` 204-        get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith`-          [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|]
− test/Main.hs
@@ -1,236 +0,0 @@-module Main where--import qualified Data.Aeson                 as JSON-import qualified Hasql.Pool                 as P-import qualified Hasql.Transaction.Sessions as HT--import Data.Function      (id)-import Data.List.NonEmpty (toList)--import Test.Hspec--import PostgREST.App             (postgrest)-import PostgREST.Config          (AppConfig (..), LogLevel (..))-import PostgREST.Config.Database (queryPgVersion)-import PostgREST.DbStructure     (queryDbStructure)-import Protolude                 hiding (toList, toS)-import Protolude.Conv            (toS)-import SpecHelper--import qualified PostgREST.AppState as AppState--import qualified Feature.AndOrParamsSpec-import qualified Feature.AsymmetricJwtSpec-import qualified Feature.AudienceJwtSecretSpec-import qualified Feature.AuthSpec-import qualified Feature.BinaryJwtSecretSpec-import qualified Feature.ConcurrentSpec-import qualified Feature.CorsSpec-import qualified Feature.DeleteSpec-import qualified Feature.DisabledOpenApiSpec-import qualified Feature.EmbedDisambiguationSpec-import qualified Feature.EmbedInnerJoinSpec-import qualified Feature.ExtraSearchPathSpec-import qualified Feature.HtmlRawOutputSpec-import qualified Feature.IgnorePrivOpenApiSpec-import qualified Feature.InsertSpec-import qualified Feature.JsonOperatorSpec-import qualified Feature.LegacyGucsSpec-import qualified Feature.MultipleSchemaSpec-import qualified Feature.NoJwtSpec-import qualified Feature.NonexistentSchemaSpec-import qualified Feature.OpenApiSpec-import qualified Feature.OptionsSpec-import qualified Feature.ProxySpec-import qualified Feature.QueryLimitedSpec-import qualified Feature.QuerySpec-import qualified Feature.RangeSpec-import qualified Feature.RawOutputTypesSpec-import qualified Feature.RollbackSpec-import qualified Feature.RootSpec-import qualified Feature.RpcPreRequestGucsSpec-import qualified Feature.RpcSpec-import qualified Feature.SingularSpec-import qualified Feature.UnicodeSpec-import qualified Feature.UpdateSpec-import qualified Feature.UpsertSpec---main :: IO ()-main = do-  testDbConn <- getEnvVarWithDefault "PGRST_DB_URI" "postgres://postgrest_test@localhost/postgrest_test"--  pool <- P.acquire (3, 10, toS testDbConn)--  actualPgVersion <- either (panic.show) id <$> P.use pool queryPgVersion--  baseDbStructure <--    loadDbStructure pool-      (configDbSchemas $ testCfg testDbConn)-      (configDbExtraSearchPath $ testCfg testDbConn)-      actualPgVersion--  let-    -- For tests that run with the same refDbStructure-    app cfg = do-      let config = cfg testDbConn-      appState <- AppState.initWithPool pool config-      AppState.putPgVersion appState actualPgVersion-      AppState.putDbStructure appState baseDbStructure-      when (isJust $ configDbRootSpec config) $-        AppState.putJsonDbS appState $ toS $ JSON.encode baseDbStructure-      return ((), postgrest LogCrit appState $ pure ())--    -- For tests that run with a different DbStructure(depends on configSchemas)-    appDbs cfg = do-      let config = cfg testDbConn-      customDbStructure <--        loadDbStructure pool-          (configDbSchemas config)-          (configDbExtraSearchPath config)-          actualPgVersion-      appState <- AppState.initWithPool pool config-      AppState.putPgVersion appState actualPgVersion-      AppState.putDbStructure appState customDbStructure-      when (isJust $ configDbRootSpec config) $-        AppState.putJsonDbS appState $ toS $ JSON.encode baseDbStructure-      return ((), postgrest LogCrit appState $ pure ())--  let withApp              = app testCfg-      maxRowsApp           = app testMaxRowsCfg-      disabledOpenApi      = app testDisabledOpenApiCfg-      proxyApp             = app testProxyCfg-      noJwtApp             = app testCfgNoJWT-      binaryJwtApp         = app testCfgBinaryJWT-      audJwtApp            = app testCfgAudienceJWT-      asymJwkApp           = app testCfgAsymJWK-      asymJwkSetApp        = app testCfgAsymJWKSet-      rootSpecApp          = app testCfgRootSpec-      htmlRawOutputApp     = app testCfgHtmlRawOutput-      responseHeadersApp   = app testCfgResponseHeaders-      disallowRollbackApp  = app testCfgDisallowRollback-      forceRollbackApp     = app testCfgForceRollback-      testCfgLegacyGucsApp = app testCfgLegacyGucs--      extraSearchPathApp   = appDbs testCfgExtraSearchPath-      unicodeApp           = appDbs testUnicodeCfg-      nonexistentSchemaApp = appDbs testNonexistentSchemaCfg-      multipleSchemaApp    = appDbs testMultipleSchemaCfg-      ignorePrivOpenApi    = appDbs testIgnorePrivOpenApiCfg--  let analyze :: IO ()-      analyze = do-        analyzeTable testDbConn "items"-        analyzeTable testDbConn "child_entities"--      specs = uncurry describe <$> [-          ("Feature.AndOrParamsSpec"         , Feature.AndOrParamsSpec.spec actualPgVersion)-        , ("Feature.AuthSpec"                , Feature.AuthSpec.spec actualPgVersion)-        , ("Feature.ConcurrentSpec"          , Feature.ConcurrentSpec.spec)-        , ("Feature.CorsSpec"                , Feature.CorsSpec.spec)-        , ("Feature.DeleteSpec"              , Feature.DeleteSpec.spec)-        , ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)-        , ("Feature.EmbedInnerJoinSpec"      , Feature.EmbedInnerJoinSpec.spec)-        , ("Feature.InsertSpec"              , Feature.InsertSpec.spec actualPgVersion)-        , ("Feature.JsonOperatorSpec"        , Feature.JsonOperatorSpec.spec actualPgVersion)-        , ("Feature.OpenApiSpec"             , Feature.OpenApiSpec.spec actualPgVersion)-        , ("Feature.OptionsSpec"             , Feature.OptionsSpec.spec actualPgVersion)-        , ("Feature.QuerySpec"               , Feature.QuerySpec.spec actualPgVersion)-        , ("Feature.RawOutputTypesSpec"      , Feature.RawOutputTypesSpec.spec)-        , ("Feature.RpcSpec"                 , Feature.RpcSpec.spec actualPgVersion)-        , ("Feature.SingularSpec"            , Feature.SingularSpec.spec)-        , ("Feature.UpdateSpec"              , Feature.UpdateSpec.spec)-        , ("Feature.UpsertSpec"              , Feature.UpsertSpec.spec actualPgVersion)-        ]--  hspec $ do-    mapM_ (parallel . before withApp) specs--    -- we analyze to get accurate results from EXPLAIN-    parallel $ beforeAll_ analyze . before withApp $-      describe "Feature.RangeSpec" Feature.RangeSpec.spec--    -- this test runs with a raw-output-media-types set to text/html-    parallel $ before htmlRawOutputApp $-      describe "Feature.HtmlRawOutputSpec" Feature.HtmlRawOutputSpec.spec--    -- this test runs with a different server flag-    parallel $ before maxRowsApp $-      describe "Feature.QueryLimitedSpec" Feature.QueryLimitedSpec.spec--    -- this test runs with a different schema-    parallel $ before unicodeApp $-      describe "Feature.UnicodeSpec" Feature.UnicodeSpec.spec--    -- this test runs with openapi-mode set to disabled-    parallel $ before disabledOpenApi $-      describe "Feature.DisabledOpenApiSpec" Feature.DisabledOpenApiSpec.spec--    -- this test runs with openapi-mode set to ignore-acl-    parallel $ before ignorePrivOpenApi $-      describe "Feature.IgnorePrivOpenApiSpec" Feature.IgnorePrivOpenApiSpec.spec--    -- this test runs with a proxy-    parallel $ before proxyApp $-      describe "Feature.ProxySpec" Feature.ProxySpec.spec--    -- this test runs without a JWT secret-    parallel $ before noJwtApp $-      describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec--    -- this test runs with a binary JWT secret-    parallel $ before binaryJwtApp $-      describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec--    -- this test runs with a binary JWT secret and an audience claim-    parallel $ before audJwtApp $-      describe "Feature.AudienceJwtSecretSpec" Feature.AudienceJwtSecretSpec.spec--    -- this test runs with asymmetric JWK-    parallel $ before asymJwkApp $-      describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec--    -- this test runs with asymmetric JWKSet-    parallel $ before asymJwkSetApp $-      describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec--    -- this test runs with a nonexistent db-schema-    parallel $ before nonexistentSchemaApp $-      describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec--    -- this test runs with an extra search path-    parallel $ before extraSearchPathApp $-      describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec--    -- this test runs with a root spec function override-    parallel $ before rootSpecApp $-      describe "Feature.RootSpec" Feature.RootSpec.spec-    parallel $ before responseHeadersApp $-      describe "Feature.RpcPreRequestGucsSpec" Feature.RpcPreRequestGucsSpec.spec--    -- this test runs with multiple schemas-    parallel $ before multipleSchemaApp $-      describe "Feature.MultipleSchemaSpec" Feature.MultipleSchemaSpec.spec--    -- this test runs with db-uses-legacy-gucs = false-    parallel $ before testCfgLegacyGucsApp $-      describe "Feature.LegacyGucsSpec" Feature.LegacyGucsSpec.spec--    -- Note: the rollback tests can not run in parallel, because they test persistance and-    -- this results in race conditions--    -- this test runs with tx-rollback-all = true and tx-allow-override = true-    before withApp $-      describe"Feature.RollbackAllowedSpec" Feature.RollbackSpec.allowed--    -- this test runs with tx-rollback-all = false and tx-allow-override = false-    before disallowRollbackApp $-      describe "Feature.RollbackDisallowedSpec" Feature.RollbackSpec.disallowed--    -- this test runs with tx-rollback-all = true and tx-allow-override = false-    before forceRollbackApp $-      describe "Feature.RollbackForcedSpec" Feature.RollbackSpec.forced--  where-    loadDbStructure pool schemas extraSearchPath actualPgVersion =-      either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ queryDbStructure (toList schemas) extraSearchPath actualPgVersion True)
− test/QueryCost.hs
@@ -1,89 +0,0 @@-module Main where--import           Control.Lens                      ((^?))-import qualified Data.Aeson.Lens                   as L-import qualified Hasql.Decoders                    as HD-import qualified Hasql.DynamicStatements.Snippet   as H-import qualified Hasql.DynamicStatements.Statement as H-import qualified Hasql.Pool                        as P-import qualified Hasql.Statement                   as H-import qualified Hasql.Transaction                 as HT-import qualified Hasql.Transaction.Sessions        as HT-import           Text.Heredoc--import Protolude      hiding (get, toS)-import Protolude.Conv (toS)--import PostgREST.Query.QueryBuilder (requestToCallProcQuery)-import PostgREST.Request.Types--import PostgREST.DbStructure.Identifiers-import PostgREST.DbStructure.Proc--import SpecHelper (getEnvVarWithDefault)--import Test.Hspec--main :: IO ()-main = do-  testDbConn <- getEnvVarWithDefault "PGRST_DB_URI" "postgres://postgrest_test@localhost/postgrest_test"-  pool <- P.acquire (3, 10, toS testDbConn)--  hspec $ describe "QueryCost" $-    context "call proc query" $ do-      it "should not exceed cost when calling setof composite proc" $ do-        cost <- exec pool $-          requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")-          (KeyParams [ProcParam "id" "int" True False])-          (Just [str| {"id": 3} |]) False False [])-        liftIO $-          cost `shouldSatisfy` (< Just 40)--      it "should not exceed cost when calling setof composite proc with empty params" $ do-        cost <- exec pool $-          requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") (KeyParams []) Nothing False False [])-        liftIO $-          cost `shouldSatisfy` (< Just 30)--      it "should not exceed cost when calling scalar proc" $ do-        cost <- exec pool $-          requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")-          (KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])-          (Just [str| {"a": 3, "b": 4} |]) True False [])-        liftIO $-          cost `shouldSatisfy` (< Just 10)--      context "params=multiple-objects" $ do-        it "should not exceed cost when calling setof composite proc" $ do-          cost <- exec pool $-            requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")-            (KeyParams [ProcParam "id" "int" True False])-            (Just [str| [{"id": 1}, {"id": 4}] |]) False True [])-          liftIO $ do-            -- lower bound needed for now to make sure that cost is not Nothing-            cost `shouldSatisfy` (> Just 2000)-            cost `shouldSatisfy` (< Just 2100)--        it "should not exceed cost when calling scalar proc" $ do-          cost <- exec pool $-            requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")-            (KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])-            (Just [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True False [])-          liftIO $-            cost `shouldSatisfy` (< Just 10)---exec :: P.Pool -> H.Snippet -> IO (Maybe Int64)-exec pool query =-  join . rightToMaybe <$>-  P.use pool (HT.transaction HT.ReadCommitted HT.Read $ HT.statement mempty $ explainCost query)--explainCost :: H.Snippet -> H.Statement () (Maybe Int64)-explainCost query =-  H.dynamicallyParameterized snippet decodeExplain False- where-   snippet = "EXPLAIN (FORMAT JSON) " <> query-   decodeExplain :: HD.Result (Maybe Int64)-   decodeExplain =-     let row = HD.singleRow $ HD.column $ HD.nonNullable HD.bytea in-     (^? L.nth 0 . L.key "Plan" .  L.key "Total Cost" . L._Integral) <$> row
− test/SpecHelper.hs
@@ -1,246 +0,0 @@-module SpecHelper where--import qualified Data.ByteString.Base64 as B64 (decodeLenient, encode)-import qualified Data.ByteString.Char8  as BS-import qualified Data.ByteString.Lazy   as BL-import qualified Data.Map.Strict        as M-import qualified Data.Set               as S-import qualified System.IO.Error        as E--import Data.Aeson           (Value (..), decode, encode)-import Data.CaseInsensitive (CI (..), original)-import Data.List            (lookup)-import Data.List.NonEmpty   (fromList)-import Network.Wai.Test     (SResponse (simpleBody, simpleHeaders, simpleStatus))-import System.Environment   (getEnv)-import System.Process       (readProcess)-import Text.Regex.TDFA      ((=~))---import Network.HTTP.Types-import Test.Hspec-import Test.Hspec.Wai-import Text.Heredoc--import PostgREST.Config                  (AppConfig (..),-                                          JSPathExp (..),-                                          LogLevel (..),-                                          OpenAPIMode (..),-                                          parseSecret)-import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))-import Protolude                         hiding (toS)-import Protolude.Conv                    (toS)--matchContentTypeJson :: MatchHeader-matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8"--matchContentTypeSingular :: MatchHeader-matchContentTypeSingular = "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"--matchHeaderAbsent :: HeaderName -> MatchHeader-matchHeaderAbsent name = MatchHeader $ \headers _body ->-  case lookup name headers of-    Just _  -> Just $ "unexpected header: " <> toS (original name) <> "\n"-    Nothing -> Nothing--validateOpenApiResponse :: [Header] -> WaiSession () ()-validateOpenApiResponse headers = do-  r <- request methodGet "/" headers ""-  liftIO $-    let respStatus = simpleStatus r in-    respStatus `shouldSatisfy`-      \s -> s == Status { statusCode = 200, statusMessage="OK" }-  liftIO $-    let respHeaders = simpleHeaders r in-    respHeaders `shouldSatisfy`-      \hs -> ("Content-Type", "application/openapi+json; charset=utf-8") `elem` hs-  let Just body = decode (simpleBody r)-  Just schema <- liftIO $ decode <$> BL.readFile "test/fixtures/openapi.json"-  let args :: M.Map Text Value-      args = M.fromList-        [ ( "schema", schema )-        , ( "data", body ) ]-      hdrs = acceptHdrs "application/json"-  request methodPost "/rpc/validate_json_schema" hdrs (encode args)-      `shouldRespondWith` "true"-      { matchStatus = 200-      , matchHeaders = []-      }---getEnvVarWithDefault :: Text -> Text -> IO Text-getEnvVarWithDefault var def = toS <$>-  getEnv (toS var) `E.catchIOError` const (return $ toS def)--_baseCfg :: AppConfig-_baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in-  AppConfig {-    configAppSettings           = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]-  , configDbAnonRole            = "postgrest_test_anonymous"-  , configDbChannel             = mempty-  , configDbChannelEnabled      = True-  , configDbExtraSearchPath     = []-  , configDbMaxRows             = Nothing-  , configDbPoolSize            = 10-  , configDbPoolTimeout         = 10-  , configDbPreRequest          = Just $ QualifiedIdentifier "test" "switch_role"-  , configDbPreparedStatements  = True-  , configDbRootSpec            = Nothing-  , configDbSchemas             = fromList ["test"]-  , configDbConfig              = False-  , configDbUri                 = mempty-  , configDbUseLegacyGucs       = True-  , configFilePath              = Nothing-  , configJWKS                  = parseSecret <$> secret-  , configJwtAudience           = Nothing-  , configJwtRoleClaimKey       = [JSPKey "role"]-  , configJwtSecret             = secret-  , configJwtSecretIsBase64     = False-  , configLogLevel              = LogCrit-  , configOpenApiMode           = OAFollowPriv-  , configOpenApiServerProxyUri = Nothing-  , configRawMediaTypes         = []-  , configServerHost            = "localhost"-  , configServerPort            = 3000-  , configServerUnixSocket      = Nothing-  , configServerUnixSocketMode  = 432-  , configDbTxAllowOverride     = True-  , configDbTxRollbackAll       = True-  }--testCfg :: Text -> AppConfig-testCfg testDbConn = _baseCfg { configDbUri = testDbConn }--testCfgDisallowRollback :: Text -> AppConfig-testCfgDisallowRollback testDbConn = (testCfg testDbConn) { configDbTxAllowOverride = False, configDbTxRollbackAll = False }--testCfgForceRollback :: Text -> AppConfig-testCfgForceRollback testDbConn = (testCfg testDbConn) { configDbTxAllowOverride = False, configDbTxRollbackAll = True }--testCfgNoJWT :: Text -> AppConfig-testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing, configJWKS = Nothing }--testUnicodeCfg :: Text -> AppConfig-testUnicodeCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["تست"] }--testMaxRowsCfg :: Text -> AppConfig-testMaxRowsCfg testDbConn = (testCfg testDbConn) { configDbMaxRows = Just 2 }--testDisabledOpenApiCfg :: Text -> AppConfig-testDisabledOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OADisabled }--testIgnorePrivOpenApiCfg :: Text -> AppConfig-testIgnorePrivOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OAIgnorePriv, configDbSchemas = fromList ["test", "v1"] }--testProxyCfg :: Text -> AppConfig-testProxyCfg testDbConn = (testCfg testDbConn) { configOpenApiServerProxyUri = Just "https://postgrest.com/openapi.json" }--testCfgBinaryJWT :: Text -> AppConfig-testCfgBinaryJWT testDbConn =-  let secret = Just . B64.decodeLenient $ "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" in-  (testCfg testDbConn) {-    configJwtSecret = secret-  , configJWKS = parseSecret <$> secret-  }--testCfgAudienceJWT :: Text -> AppConfig-testCfgAudienceJWT testDbConn =-  let secret = Just . B64.decodeLenient $ "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" in-  (testCfg testDbConn) {-    configJwtSecret = secret-  , configJwtAudience = Just "youraudience"-  , configJWKS = parseSecret <$> secret-  }--testCfgAsymJWK :: Text -> AppConfig-testCfgAsymJWK testDbConn =-  let secret = Just $ encodeUtf8 [str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|]-  in (testCfg testDbConn) {-    configJwtSecret = secret-  , configJWKS = parseSecret <$> secret-  }--testCfgAsymJWKSet :: Text -> AppConfig-testCfgAsymJWKSet testDbConn =-  let secret = Just $ encodeUtf8 [str|{"keys": [{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}]}|]-  in (testCfg testDbConn) {-    configJwtSecret = secret-  , configJWKS = parseSecret <$> secret-  }--testNonexistentSchemaCfg :: Text -> AppConfig-testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["nonexistent"] }--testCfgExtraSearchPath :: Text -> AppConfig-testCfgExtraSearchPath testDbConn = (testCfg testDbConn) { configDbExtraSearchPath = ["public", "extensions"] }--testCfgRootSpec :: Text -> AppConfig-testCfgRootSpec testDbConn = (testCfg testDbConn) { configDbRootSpec = Just $ QualifiedIdentifier mempty "root"}--testCfgHtmlRawOutput :: Text -> AppConfig-testCfgHtmlRawOutput testDbConn = (testCfg testDbConn) { configRawMediaTypes = ["text/html"] }--testCfgResponseHeaders :: Text -> AppConfig-testCfgResponseHeaders testDbConn = (testCfg testDbConn) { configDbPreRequest = Just $ QualifiedIdentifier mempty "custom_headers" }--testMultipleSchemaCfg :: Text -> AppConfig-testMultipleSchemaCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["v1", "v2"] }--testCfgLegacyGucs :: Text -> AppConfig-testCfgLegacyGucs testDbConn = (testCfg testDbConn) { configDbUseLegacyGucs = False }--resetDb :: Text -> IO ()-resetDb dbConn = loadFixture dbConn "data"--analyzeTable :: Text -> Text -> IO ()-analyzeTable dbConn tableName =-  void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", toS dbConn, "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] []--loadFixture :: Text -> FilePath -> IO()-loadFixture dbConn name =-  void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", toS dbConn, "-q", "-f", "test/fixtures/" ++ name ++ ".sql"] []--rangeHdrs :: ByteRange -> [Header]-rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]--rangeHdrsWithCount :: ByteRange -> [Header]-rangeHdrsWithCount r = ("Prefer", "count=exact") : rangeHdrs r--acceptHdrs :: BS.ByteString -> [Header]-acceptHdrs mime = [(hAccept, mime)]--rangeUnit :: Header-rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")--matchHeader :: CI BS.ByteString -> BS.ByteString -> [Header] -> Bool-matchHeader name valRegex headers =-  maybe False (=~ valRegex) $ lookup name headers--noBlankHeader :: [Header] -> Bool-noBlankHeader = notElem mempty--noProfileHeader :: [Header] -> Bool-noProfileHeader headers = isNothing $ find ((== "Content-Profile") . fst) headers--authHeader :: BS.ByteString -> BS.ByteString -> Header-authHeader typ creds =-  (hAuthorization, typ <> " " <> creds)--authHeaderBasic :: BS.ByteString -> BS.ByteString -> Header-authHeaderBasic u p =-  authHeader "Basic" $ toS . B64.encode . toS $ u <> ":" <> p--authHeaderJWT :: BS.ByteString -> Header-authHeaderJWT = authHeader "Bearer"---- | Tests whether the text can be parsed as a json object containing--- the key "message", and optional keys "details", "hint", "code",--- and no extraneous keys-isErrorFormat :: BL.ByteString -> Bool-isErrorFormat s =-  "message" `S.member` keys &&-    S.null (S.difference keys validKeys)- where-  obj = decode s :: Maybe (M.Map Text Value)-  keys = maybe S.empty M.keysSet obj-  validKeys = S.fromList ["message", "details", "hint", "code"]
− test/TestTypes.hs
@@ -1,37 +0,0 @@-module TestTypes (-  IncPK(..)-, CompoundPK(..)-) where--import           Data.Aeson ((.:))-import qualified Data.Aeson as JSON--import Protolude--data IncPK = IncPK {-  incId          :: Int-, incNullableStr :: Maybe Text-, incStr         :: Text-, incInsert      :: Text-} deriving (Eq, Show)--instance JSON.FromJSON IncPK where-  parseJSON (JSON.Object r) = IncPK <$>-    r .: "id" <*>-    r .: "nullable_string" <*>-    r .: "non_nullable_string" <*>-    r .: "inserted_at"-  parseJSON _ = mzero--data CompoundPK = CompoundPK {-  compoundK1    :: Int-, compoundK2    :: Text-, compoundExtra :: Maybe Int-} deriving (Eq, Show)--instance JSON.FromJSON CompoundPK where-  parseJSON (JSON.Object r) = CompoundPK <$>-    r .: "k1" <*>-    r .: "k2" <*>-    r .: "extra"-  parseJSON _ = mzero
+ test/doc/Main.hs view
@@ -0,0 +1,17 @@+module Main (main) where++import Test.DocTest (doctest)++import Protolude+++main :: IO ()+main =+  doctest+    [ "--verbose"+    , "-XOverloadedStrings"+    , "-XNoImplicitPrelude"+    , "-XStandaloneDeriving"+    , "-isrc"+    , "src/PostgREST/Request/Preferences.hs"+    ]
− test/doctests/Main.hs
@@ -1,17 +0,0 @@-module Main (main) where--import Test.DocTest (doctest)--import Protolude---main :: IO ()-main =-  doctest-    [ "--verbose"-    , "-XOverloadedStrings"-    , "-XNoImplicitPrelude"-    , "-XStandaloneDeriving"-    , "-isrc"-    , "src/PostgREST/Request/Preferences.hs"-    ]
+ test/spec/Feature/AndOrParamsSpec.hs view
@@ -0,0 +1,276 @@+module Feature.AndOrParamsSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import PostgREST.Config.PgVersion (PgVersion, pgVersion112)++import Protolude  hiding (get)+import SpecHelper++spec :: PgVersion -> SpecWith ((), Application)+spec actualPgVersion =+  describe "and/or params used for complex boolean logic" $ do+    context "used with GET" $ do+      context "or param" $ do+        it "can do simple logic" $+          get "/entities?or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can negate simple logic" $+          get "/entities?not.or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`+            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can be combined with traditional filters" $+          get "/entities?or=(id.eq.1,id.eq.2)&name=eq.entity 1&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }++      context "embedded levels" $ do+        it "can do logic on the second level" $+          get "/entities?child_entities.or=(id.eq.1,name.eq.child entity 2)&select=id,child_entities(id)" `shouldRespondWith`+            [json|[+              {"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []},+              {"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}+            ]|] { matchHeaders = [matchContentTypeJson] }++        it "can do logic on the third level" $+          get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))"+            `shouldRespondWith`+              [json|[+                {"id": 1, "child_entities": [+                  { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]},+                  { "id": 2, "grandchild_entities": []},+                  { "id": 4, "grandchild_entities": []},+                  { "id": 5, "grandchild_entities": []}+                ]},+                {"id": 2, "child_entities": [+                  { "id": 3, "grandchild_entities": []},+                  { "id": 6, "grandchild_entities": []}+                ]},+                {"id": 3, "child_entities": []},+                {"id": 4, "child_entities": []}+              ]|]++      context "and/or params combined" $ do+        it "can be nested inside the same expression" $+          get "/entities?or=(and(name.eq.entity 2,id.eq.2),and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can be negated while nested" $+          get "/entities?or=(not.and(name.eq.entity 2,id.eq.2),not.and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can be combined unnested" $+          get "/entities?and=(id.eq.1,name.eq.entity 1)&or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }++      context "operators inside and/or" $ do+        it "can handle eq and neq" $+          get "/entities?and=(id.eq.1,id.neq.2))&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can handle lt and gt" $+          get "/entities?or=(id.lt.2,id.gt.3)&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can handle lte and gte" $+          get "/entities?or=(id.lte.2,id.gte.3)&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can handle like and ilike" $+          get "/entities?or=(name.like.*1,name.ilike.*ENTITY 2)&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can handle in" $+          get "/entities?or=(id.in.(1,2),id.in.(3,4))&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can handle is" $+          get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith`+            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+        it "can handle fts" $ do+          get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/tsearch?or=(text_search_vector.plfts(german).Art%20Spass, text_search_vector.plfts(french).amusant%20impossible, text_search_vector.fts(english).impossible)" `shouldRespondWith`+            [json|[+              {"text_search_vector": "'fun':5 'imposs':9 'kind':3" },+              {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },+              {"text_search_vector": "'art':4 'spass':5 'unmog':7"}+            ]|] { matchHeaders = [matchContentTypeJson] }++        when (actualPgVersion >= pgVersion112) $+          it "can handle wfts (websearch_to_tsquery)" $+            get "/tsearch?or=(text_search_vector.plfts(german).Art,text_search_vector.plfts(french).amusant,text_search_vector.not.wfts(english).impossible)"+            `shouldRespondWith`+              [json|[+                     {"text_search_vector": "'also':2 'fun':3 'possibl':8" },+                     {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" },+                     {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },+                     {"text_search_vector": "'art':4 'spass':5 'unmog':7" }+              ]|]+              { matchHeaders = [matchContentTypeJson] }++        it "can handle cs and cd" $+          get "/entities?or=(arr.cs.{1,2,3},arr.cd.{1})&select=id" `shouldRespondWith`+            [json|[{ "id": 1 },{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }++        it "can handle range operators" $ do+          get "/ranges?range=eq.[1,3]&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=neq.[1,3]&select=id" `shouldRespondWith`+            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=lt.[1,10]&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=gt.[8,11]&select=id" `shouldRespondWith`+            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=lte.[1,3]&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=gte.[2,3]&select=id" `shouldRespondWith`+            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=cs.[1,2]&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=cd.[1,6]&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=ov.[0,4]&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=sl.[9,10]&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=sr.[3,4]&select=id" `shouldRespondWith`+            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=nxr.[4,7]&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=nxl.[4,7]&select=id" `shouldRespondWith`+            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/ranges?range=adj.(3,10]&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }++        it "can handle array operators" $ do+          get "/entities?arr=eq.{1,2,3}&select=id" `shouldRespondWith`+            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=neq.{1,2}&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=lt.{2,3}&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=lt.{2,0}&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=gt.{1,1}&select=id" `shouldRespondWith`+            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=gt.{3}&select=id" `shouldRespondWith`+            [json|[]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=lte.{2,1}&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=lte.{1,2,3}&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=lte.{1,2}&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=cs.{1,2}&select=id" `shouldRespondWith`+            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=cd.{1,2,6}&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=ov.{3}&select=id" `shouldRespondWith`+            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?arr=ov.{2,3}&select=id" `shouldRespondWith`+            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }++        context "operators with not" $ do+          it "eq, cs, like can be negated" $+            get "/entities?and=(arr.not.cs.{1,2,3},and(id.not.eq.2,name.not.like.*3))&select=id" `shouldRespondWith`+              [json|[{ "id": 1}]|] { matchHeaders = [matchContentTypeJson] }+          it "in, is, fts can be negated" $+            get "/entities?and=(id.not.in.(1,3),and(name.not.is.null,text_search_vector.not.fts.foo))&select=id" `shouldRespondWith`+              [json|[{ "id": 2}]|] { matchHeaders = [matchContentTypeJson] }+          it "lt, gte, cd can be negated" $+            get "/entities?and=(arr.not.cd.{1},or(id.not.lt.1,id.not.gte.3))&select=id" `shouldRespondWith`+              [json|[{"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }+          it "gt, lte, ilike can be negated" $+            get "/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id" `shouldRespondWith`+              [json|[{"id": 1}, {"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }++      context "and/or params with quotes" $ do+        it "eq can have quotes" $+          get "/grandchild_entities?or=(name.eq.\"(grandchild,entity,4)\",name.eq.\"(grandchild,entity,5)\")&select=id" `shouldRespondWith`+            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }+        it "like and ilike can have quotes" $+          get "/grandchild_entities?or=(name.like.\"*ity,4*\",name.ilike.\"*ITY,5)\")&select=id" `shouldRespondWith`+            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }+        it "in can have quotes" $+          get "/grandchild_entities?or=(id.in.(\"1\",\"2\"),id.in.(\"3\",\"4\"))&select=id" `shouldRespondWith`+            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }++      it "allows whitespace" $+        get "/entities?and=( and ( id.in.( 1, 2, 3 ) , id.eq.3 ) , or ( id.eq.2 , id.eq.3 ) )&select=id" `shouldRespondWith`+          [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }++      context "multiple and/or conditions" $ do+        it "cannot have zero conditions" $+          get "/entities?or=()" `shouldRespondWith`+            [json|{+              "details": "unexpected \")\" expecting field name (* or [a..z0..9_]), negation operator (not) or logic operator (and, or)",+              "message": "\"failed to parse logic tree (())\" (line 1, column 4)"+            }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }+        it "can have a single condition" $ do+          get "/entities?or=(id.eq.1)&select=id" `shouldRespondWith`+            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }+          get "/entities?and=(id.eq.1)&select=id" `shouldRespondWith`+            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }+        it "can have three conditions" $ do+          get "/grandchild_entities?or=(id.eq.1, id.eq.2, id.eq.3)&select=id" `shouldRespondWith`+            [json|[{"id":1}, {"id":2}, {"id":3}]|] { matchHeaders = [matchContentTypeJson] }+          get "/grandchild_entities?and=(id.in.(1,2), id.in.(3,1), id.in.(1,4))&select=id" `shouldRespondWith`+            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }+        it "can have four conditions combining and/or" $ do+          get "/grandchild_entities?or=( id.eq.1, id.eq.2, and(id.in.(1,3), id.in.(2,3)), id.eq.4 )&select=id" `shouldRespondWith`+            [json|[{"id":1}, {"id":2}, {"id":3}, {"id":4}]|] { matchHeaders = [matchContentTypeJson] }+          get "/grandchild_entities?and=( id.eq.1, not.or(id.eq.2, id.eq.3), id.in.(1,4), or(id.eq.1, id.eq.4) )&select=id" `shouldRespondWith`+            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }++    context "used with POST" $+      it "includes related data with filters" $+        request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id"+            [("Prefer", "return=representation")]+            [json|[+              {"id":7,"name":"entity 4","parent_id":1},+              {"id":8,"name":"entity 5","parent_id":2},+              {"id":9,"name":"entity 6","parent_id":3}+            ]|]+          `shouldRespondWith`+            [json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|]+            { matchStatus = 201 }++    context "used with PATCH" $+      it "succeeds when using and/or params" $+        request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"+          [("Prefer", "return=representation")]+          [json|{ name : "updated grandchild entity"}|] `shouldRespondWith`+          [json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]+          { matchHeaders = [matchContentTypeJson] }++    context "used with DELETE" $+      it "succeeds when using and/or params" $+        request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"+            [("Prefer", "return=representation")]+            ""+          `shouldRespondWith`+            [json|[{ "id": 1, "name" : "grandchild entity 1" },{ "id": 2, "name" : "grandchild entity 2" }]|]++    it "can query columns that begin with and/or reserved words" $+      get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200++    it "fails when using IN without () and provides meaningful error message" $+      get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`+        [json|{+          "details": "unexpected \"1\" expecting \"(\"",+          "message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)"+        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }++    it "fails on malformed query params and provides meaningful error message" $ do+      get "/entities?or=)(" `shouldRespondWith`+        [json|{+          "details": "unexpected \")\" expecting \"(\"",+          "message": "\"failed to parse logic tree ()()\" (line 1, column 3)"+        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }+      get "/entities?and=(ord(id.eq.1,id.eq.1),id.eq.2)" `shouldRespondWith`+        [json|{+          "details": "unexpected \"d\" expecting \"(\"",+          "message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)"+        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }+      get "/entities?or=(id.eq.1,not.xor(id.eq.2,id.eq.3))" `shouldRespondWith`+        [json|{+          "details": "unexpected \"x\" expecting logic operator (and, or)",+          "message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)"+        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
+ test/spec/Feature/AsymmetricJwtSpec.hs view
@@ -0,0 +1,21 @@+module Feature.AsymmetricJwtSpec where++-- {{{ Imports+import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai++import Protolude+import SpecHelper+-- }}}++spec :: SpecWith ((), Application)+spec = describe "server started with asymmetric JWK" $++  -- this test will stop working 9999999999s after the UNIX EPOCH+  it "succeeds with jwt token signed with an asymmetric key" $ do+    let auth = authHeaderJWT "eyJhbGciOiJSUzI1NiJ9.eyJyb2xlIjogInBvc3RncmVzdF90ZXN0X2F1dGhvciJ9Cg.CBOYWDvqgAR0YYnZnyDGTQi6AJLc2Pds6_eV3YuBG6I36mj_h05eLhkEKNEDA5ZteMzCiY83P60rC_xtxVd7B6vo3BeF5uoanPS3rrbuHzKPwzsrgrD_CqvEuJ4n7Q9epkQiLsNkcexneENZDRqFjbwZx3DrXiCWwlK3Ytr5NAIGxmy0od-0xNpb2U1nXQyO_Q3mumWFViRt4tmFn_3goDHNKG3Ha_AzImfUNvHnWL78kAc4rbn15vLtWXD8PwtSnZaB4lY4V6RfsaW937srQsmRetvytM1i_bHBnjkjQLAqGbXPyItjtlXPs0uGNBadE8-wgkLtfmSCC4v2DjUthw"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 200
+ test/spec/Feature/AudienceJwtSecretSpec.hs view
@@ -0,0 +1,47 @@+module Feature.AudienceJwtSecretSpec where++-- {{{ Imports+import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai++import Protolude  hiding (get)+import SpecHelper+-- }}}++spec :: SpecWith ((), Application)+spec = describe "test handling of aud claims in JWT" $ do++  -- this test will stop working 9999999999s after the UNIX EPOCH+  it "succeeds with jwt token containing with an audience claim" $ do+    {-  This is the decoded contents of authHeaderJWT++        {+          "exp": 9999999999,+          "role": "postgrest_test_author",+          "id": "jdoe",+          "aud": "youraudience"+        }++    -}+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UiLCJhdWQiOiJ5b3VyYXVkaWVuY2UifQ.fJ4tLKSmolWGWehWN20qiU9dMO-WY0RI2VvacL7-ZGo"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 200++  it "succeeds with jwt token that does not contain an audience claim" $ do+    {- This is the decoded contents of authHeaderJWT++        {+          "exp": 9999999999,+          "role": "postgrest_test_author",+          "id": "jdoe"+        }+    -}+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 200++  it "requests without JWT token should work" $+    get "/has_count_column" `shouldRespondWith` 200
+ test/spec/Feature/AuthSpec.hs view
@@ -0,0 +1,179 @@+module Feature.AuthSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import PostgREST.Config.PgVersion (PgVersion, pgVersion112)++import Protolude  hiding (get)+import SpecHelper++spec :: PgVersion -> SpecWith ((), Application)+spec actualPgVersion = describe "authorization" $ do+  let single = ("Accept","application/vnd.pgrst.object+json")++  it "denies access to tables that anonymous does not own" $+    get "/authors_only" `shouldRespondWith` (+        if actualPgVersion >= pgVersion112 then+        [json| {+          "hint":null,+          "details":null,+          "code":"42501",+          "message":"permission denied for table authors_only"} |]+           else+        [json| {+          "hint":null,+          "details":null,+          "code":"42501",+          "message":"permission denied for relation authors_only"} |]+                                            )+      { matchStatus = 401+      , matchHeaders = ["WWW-Authenticate" <:> "Bearer"]+      }++  it "denies access to tables that postgrest_test_author does not own" $+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" in+    request methodGet "/private_table" [auth] ""+      `shouldRespondWith` (+        if actualPgVersion >= pgVersion112 then+        [json| {+          "hint":null,+          "details":null,+          "code":"42501",+          "message":"permission denied for table private_table"} |]+           else+        [json| {+          "hint":null,+          "details":null,+          "code":"42501",+          "message":"permission denied for relation private_table"} |]+                          )+      { matchStatus = 403+      , matchHeaders = []+      }++  it "denies execution on functions that anonymous does not own" $+    post "/rpc/privileged_hello" [json|{"name": "anonymous"}|] `shouldRespondWith` 401++  it "allows execution on a function that postgrest_test_author owns" $+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" in+    request methodPost "/rpc/privileged_hello" [auth] [json|{"name": "jdoe"}|]+      `shouldRespondWith` [json|"Privileged hello to jdoe"|]+      { matchStatus = 200+      , matchHeaders = [matchContentTypeJson]+      }++  it "returns jwt functions as jwt tokens" $+    request methodPost "/rpc/login" [single]+      [json| { "id": "jdoe", "pass": "1234" } |]+      `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.KO-0PGp_rU-utcDBP6qwdd-Th2Fk-ICVt01I7QtTDWs"} |]+        { matchStatus = 200+        , matchHeaders = [matchContentTypeSingular]+        }++  it "sql functions can encode custom and standard claims" $+    request methodPost  "/rpc/jwt_test" [single] "{}"+      `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.G2REtPnOQMUrVRDA9OnkPJTd8R0tf4wdYOlauh1E2Ek"} |]+        { matchStatus = 200+        , matchHeaders = [matchContentTypeSingular]+        }++  it "sql functions can read custom and standard claims variables" $ do+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwfQ.V5fEpXfpb7feqwVqlcDleFdKu86bdwU2cBRT4fcMhXg"+    request methodPost "/rpc/reveal_big_jwt" [auth] "{}"+      `shouldRespondWith` [json|[{"iss":"joe","sub":"fun","exp":9999999999,"nbf":1300819380,"iat":1300819380,"jti":"foo","http://postgrest.com/foo":true}]|]++  it "allows users with permissions to see their tables" $ do+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 200++  it "works with tokens which have extra fields" $ do+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.b0eglDKYEmGi-hCvD-ddSqFl7vnDO5qkUaviaHXm3es"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 200++  -- this test will stop working 9999999999s after the UNIX EPOCH+  it "succeeds with an unexpired token" $ do+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 200++  it "fails with an expired token" $ do+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.f8__E6VQwYcDqwHmr9PG03uaZn8Zh1b0vbJ9DYS0AdM"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` [json| {"message":"JWT expired"} |]+        { matchStatus = 401+        , matchHeaders = [+            "WWW-Authenticate" <:>+            "Bearer error=\"invalid_token\", error_description=\"JWT expired\""+          ]+        }++  it "hides tables from users with invalid JWT" $ do+    let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` [json| {"message":"JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 2)"} |]+        { matchStatus = 401+        , matchHeaders = [+            "WWW-Authenticate" <:>+            "Bearer error=\"invalid_token\", error_description=\"JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 2)\""+          ]+        }++  it "should fail when jwt contains no claims" $ do+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.CUIP5V9thWsGGFsFyGijSZf1fJMfarLHI9CEJL-TGNk"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 401++  it "hides tables from users with JWT that contain no claims about role" $ do+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.RVlZDaSyKbFPvxUf3V_NQXybfRB4dlBIkAUQXVXLUAI"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 401++  it "recovers after 401 error with logged in user" $ do+    _ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |]+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"+    _ <- request methodPost "/rpc/problem" [auth] ""+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 200++  describe "custom pre-request proc acting on id claim" $ do++    it "able to switch to postgrest_test_author role (id=1)" $+      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.gKw7qI50i9hMrSJW8BlTpdMEVmMXJYxlAqueGqpa_mE" in+      request methodPost "/rpc/get_current_user" [auth]+        [json| {} |]+         `shouldRespondWith` [json|"postgrest_test_author"|]+          { matchStatus = 200+          , matchHeaders = []+          }++    it "able to switch to postgrest_test_default_role (id=2)" $+      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.nwzjMI0YLvVGJQTeoCPEBsK983b__gxdpLXisBNaO2A" in+      request methodPost "/rpc/get_current_user" [auth]+        [json| {} |]+         `shouldRespondWith` [json|"postgrest_test_default_role"|]+          { matchStatus = 200+          , matchHeaders = []+          }++    it "raises error (id=3)" $+      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.OGxEJAf60NKZiTn-tIb2jy4rqKs_ZruLGWZ40TjrJsM" in+      request methodPost "/rpc/get_current_user" [auth]+        [json| {} |]+         `shouldRespondWith` [json|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|]+          { matchStatus = 400+          , matchHeaders = []+          }++  it "allows 'Bearer' and 'bearer' as authentication schemes" $ do+    let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"+    request methodGet "/authors_only" [authHeader "Bearer" token] ""+      `shouldRespondWith` 200+    request methodGet "/authors_only" [authHeader "bearer" token] ""+      `shouldRespondWith` 200
+ test/spec/Feature/BinaryJwtSecretSpec.hs view
@@ -0,0 +1,21 @@+module Feature.BinaryJwtSecretSpec where++-- {{{ Imports+import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai++import Protolude+import SpecHelper+-- }}}++spec :: SpecWith ((), Application)+spec = describe "server started with binary JWT secret" $++  -- this test will stop working 9999999999s after the UNIX EPOCH+  it "succeeds with jwt token encoded with a binary secret" $ do+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith` 200
+ test/spec/Feature/ConcurrentSpec.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies          #-}+{-# LANGUAGE UndecidableInstances  #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+module Feature.ConcurrentSpec where++import Control.Concurrent.Async (mapConcurrently)+import Network.Wai              (Application)++import Control.Monad.Base+import Control.Monad.Trans.Control++import Network.Wai.Test        (Session)+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.Internal+import Test.Hspec.Wai.JSON++import Protolude hiding (get)++spec :: SpecWith ((), Application)+spec =+  describe "Querying in parallel" $+    it "should not raise 'transaction in progress' error" $+      raceTest 10 $+        get "/fakefake"+          `shouldRespondWith` [json|+              { "hint": null,+                "details":null,+                "code":"42P01",+                "message":"relation \"test.fakefake\" does not exist"+              } |]+          { matchStatus  = 404+          , matchHeaders = []+          }++raceTest :: Int -> WaiExpectation st -> WaiExpectation st+raceTest times = liftBaseDiscard go+ where+  go test = void $ mapConcurrently (const test) [1..times]++instance MonadBaseControl IO (WaiSession st) where+  type StM (WaiSession st) a = StM Session a+  liftBaseWith f = WaiSession $+    liftBaseWith $ \runInBase ->+      f $ \k -> runInBase (unWaiSession k)+  restoreM = WaiSession . restoreM+  {-# INLINE liftBaseWith #-}+  {-# INLINE restoreM #-}++instance MonadBase IO (WaiSession st) where+  liftBase = liftIO
+ test/spec/Feature/CorsSpec.hs view
@@ -0,0 +1,56 @@+module Feature.CorsSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai++import Protolude++spec :: SpecWith ((), Application)+spec =+  describe "CORS" $ do+    it "replies naively and permissively to preflight request" $+      request methodOptions "/"+          [ ("Accept", "*/*")+          , ("Origin", "http://example.com")+          , ("Access-Control-Request-Method", "POST")+          , ("Access-Control-Request-Headers", "Foo,Bar") ]+          ""+        `shouldRespondWith`+          ""+          { matchHeaders = [ "Access-Control-Allow-Origin" <:> "http://example.com"+                           , "Access-Control-Allow-Credentials" <:> "true"+                           , "Access-Control-Allow-Methods" <:> "GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD"+                           , "Access-Control-Allow-Headers" <:> "Authorization, Foo, Bar, Accept, Accept-Language, Content-Language"+                           , "Access-Control-Max-Age" <:> "86400" ]+          }++    it "exposes necesssary response headers to regular request" $+      request methodGet "/items"+          [("Origin", "http://example.com")]+          ""+        `shouldRespondWith`+          ResponseMatcher+          { matchStatus = 200+          , matchBody = MatchBody (\_ _ -> Nothing) -- match any body+          , matchHeaders = [ "Access-Control-Expose-Headers" <:>+                             "Content-Encoding, Content-Location, Content-Range, Content-Type, \+                             \Date, Location, Server, Transfer-Encoding, Range-Unit"]+          }++    it "allows INFO body through even with CORS request headers present to postflight request" $+      request methodOptions "/items"+          [ ("Host", "localhost:3000")+          , ("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0")+          , ("Origin", "http://localhost:8000")+          , ("Accept", "text/csv, */*; q=0.01")+          , ("Accept-Language", "en-US,en;q=0.5")+          , ("Accept-Encoding", "gzip, deflate")+          , ("Referer", "http://localhost:8000/")+          , ("Connection", "keep-alive") ]+          ""+        `shouldRespondWith`+          ""+          { matchHeaders = [ "Access-Control-Allow-Origin" <:> "*" ] }
+ test/spec/Feature/DeleteSpec.hs view
@@ -0,0 +1,92 @@+module Feature.DeleteSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude hiding (get)++spec :: SpecWith ((), Application)+spec =+  describe "Deleting" $ do+    context "existing record" $ do+      it "succeeds with 204 and deletion count" $+        request methodDelete "/items?id=eq.1" [] ""+          `shouldRespondWith` ""+          { matchStatus  = 204+          , matchHeaders = ["Content-Range" <:> "*/*"]+          }++      it "returns the deleted item and count if requested" $+        request methodDelete "/items?id=eq.2" [("Prefer", "return=representation"), ("Prefer", "count=exact")] ""+          `shouldRespondWith` [json|[{"id":2}]|]+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "*/1"]+          }++      it "ignores ?select= when return not set or return=minimal" $ do+        request methodDelete "/items?id=eq.3&select=id" [] ""+          `shouldRespondWith` ""+          { matchStatus  = 204+          , matchHeaders = ["Content-Range" <:> "*/*"]+          }+        request methodDelete "/items?id=eq.3&select=id" [("Prefer", "return=minimal")] ""+          `shouldRespondWith` ""+          { matchStatus  = 204+          , matchHeaders = ["Content-Range" <:> "*/*"]+          }++      it "returns the deleted item and shapes the response" $+        request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""+          `shouldRespondWith` [json|[{"id":2,"name":"Two"}]|]+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "*/*"]+          }++      it "can rename and cast the selected columns" $+        request methodDelete "/complex_items?id=eq.3&select=ciId:id::text,ciName:name" [("Prefer", "return=representation")] ""+          `shouldRespondWith` [json|[{"ciId":"3","ciName":"Three"}]|]++      it "can embed (parent) entities" $+        request methodDelete "/tasks?id=eq.8&select=id,name,project:projects(id)" [("Prefer", "return=representation")] ""+          `shouldRespondWith` [json|[{"id":8,"name":"Code OSX","project":{"id":4}}]|]+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "*/*"]+          }++    context "known route, no records matched" $+      it "includes [] body if return=rep" $+        request methodDelete "/items?id=eq.101"+          [("Prefer", "return=representation")] ""+          `shouldRespondWith` "[]"+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "*/*"]+          }++    context "totally unknown route" $+      it "fails with 404" $+        request methodDelete "/foozle?id=eq.101" [] "" `shouldRespondWith` 404++    context "table with limited privileges" $ do+      it "fails deleting the row when return=representation and selecting all the columns" $+        request methodDelete "/app_users?id=eq.1" [("Prefer", "return=representation")] mempty+            `shouldRespondWith` 401++      it "succeeds deleting the row when return=representation and selecting only the privileged columns" $+        request methodDelete "/app_users?id=eq.1&select=id,email" [("Prefer", "return=representation")]+          [json| { "password": "passxyz" } |]+            `shouldRespondWith` [json|[ { "id": 1, "email": "test@123.com" } ]|]+            { matchStatus  = 200+            , matchHeaders = ["Content-Range" <:> "*/*"]+            }++      it "suceeds deleting the row with no explicit select when using return=minimal" $+        request methodDelete "/app_users?id=eq.2" [("Prefer", "return=minimal")] mempty+            `shouldRespondWith` 204++      it "suceeds deleting the row with no explicit select by default" $+        request methodDelete "/app_users?id=eq.3" [] mempty+            `shouldRespondWith` 204
+ test/spec/Feature/DisabledOpenApiSpec.hs view
@@ -0,0 +1,20 @@+module Feature.DisabledOpenApiSpec where++import Network.HTTP.Types+import Network.Wai        (Application)++import Test.Hspec     hiding (pendingWith)+import Test.Hspec.Wai++import Protolude++spec :: SpecWith ((), Application)+spec =+  describe "Disabled OpenApi" $ do+    it "does not accept application/openapi+json and responds with 415" $+      request methodGet "/"+        [("Accept","application/openapi+json")] "" `shouldRespondWith` 415++    it "accepts application/json and responds with 404" $+      request methodGet "/"+        [("Accept","application/json")] "" `shouldRespondWith` 404
+ test/spec/Feature/EmbedDisambiguationSpec.hs view
@@ -0,0 +1,464 @@+module Feature.EmbedDisambiguationSpec where++import Network.Wai (Application)++import Test.Hspec          hiding (pendingWith)+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude  hiding (get)+import SpecHelper++spec :: SpecWith ((), Application)+spec =+  describe "resource embedding disambiguation" $ do+    context "ambiguous requests that give 300 Multiple Choices" $ do+      it "errs when there's a table and view that point to the same fk" $+        get "/message?select=id,body,sender(name,sent)" `shouldRespondWith`+          [json|+            {+              "details": [+                {+                    "cardinality": "many-to-one",+                    "relationship": "message_sender_fkey using message(sender) and person(id)",+                    "embedding": "message with person"+                },+                {+                    "cardinality": "many-to-one",+                    "relationship": "message_sender_fkey using message(sender) and person_detail(id)",+                    "embedding": "message with person_detail"+                }+              ],+              "hint": "Try changing 'sender' to one of the following: 'person!message_sender_fkey', 'person_detail!message_sender_fkey'. Find the desired relationship in the 'details' key.",+              "message": "Could not embed because more than one relationship was found for 'message' and 'sender'"+            }+          |]+          { matchStatus  = 300+          , matchHeaders = [matchContentTypeJson]+          }++      it "errs when there's a table and view that point to the same fk (composite pk)" $+        get "/activities?select=fst_shift(*)" `shouldRespondWith`+          [json|+            {+              "details": [+                {+                    "cardinality": "one-to-many",+                    "relationship": "fst_shift using activities(id, schedule_id) and unit_workdays(fst_shift_activity_id, fst_shift_schedule_id)",+                    "embedding": "activities with unit_workdays"+                },+                {+                    "cardinality": "one-to-many",+                    "relationship": "fst_shift using activities(id, schedule_id) and unit_workdays_fst_shift(fst_shift_activity_id, fst_shift_schedule_id)",+                    "embedding": "activities with unit_workdays_fst_shift"+                }+              ],+              "hint": "Try changing 'fst_shift' to one of the following: 'unit_workdays!fst_shift', 'unit_workdays_fst_shift!fst_shift'. Find the desired relationship in the 'details' key.",+              "message": "Could not embed because more than one relationship was found for 'activities' and 'fst_shift'"+            }+          |]+          { matchStatus  = 300+          , matchHeaders = [matchContentTypeJson]+          }++      it "errs when there are o2m and m2m cardinalities to the target table" $+        get "/sites?select=*,big_projects(*)" `shouldRespondWith`+          [json|+            {+              "details": [+                {+                  "cardinality": "many-to-one",+                  "relationship": "main_project using sites(main_project_id) and big_projects(big_project_id)",+                  "embedding": "sites with big_projects"+                },+                {+                  "cardinality": "many-to-many",+                  "relationship": "jobs using jobs_site_id_fkey(site_id) and jobs_big_project_id_fkey(big_project_id)",+                  "embedding": "sites with big_projects"+                },+                {+                  "cardinality": "many-to-many",+                  "relationship": "main_jobs using jobs_site_id_fkey(site_id) and jobs_big_project_id_fkey(big_project_id)",+                  "embedding": "sites with big_projects"+                }+              ],+              "hint": "Try changing 'big_projects' to one of the following: 'big_projects!main_project', 'big_projects!jobs', 'big_projects!main_jobs'. Find the desired relationship in the 'details' key.",+              "message": "Could not embed because more than one relationship was found for 'sites' and 'big_projects'"+            }+          |]+          { matchStatus  = 300+          , matchHeaders = [matchContentTypeJson]+          }++      it "errs on an ambiguous embed that has a circular reference" $+        get "/agents?select=*,departments(*)" `shouldRespondWith`+          [json|+            {+              "details": [+                {+                    "cardinality": "many-to-one",+                    "relationship": "agents_department_id_fkey using agents(department_id) and departments(id)",+                    "embedding": "agents with departments"+                },+                {+                    "cardinality": "one-to-many",+                    "relationship": "departments_head_id_fkey using agents(id) and departments(head_id)",+                    "embedding": "agents with departments"+                }+              ],+              "hint": "Try changing 'departments' to one of the following: 'departments!agents_department_id_fkey', 'departments!departments_head_id_fkey'. Find the desired relationship in the 'details' key.",+              "message": "Could not embed because more than one relationship was found for 'agents' and 'departments'"+            }+           |]+          { matchStatus  = 300+          , matchHeaders = [matchContentTypeJson]+          }++      it "errs when there are more than two fks on a junction table(currently impossible to disambiguate, only choice is to split the table)" $+        -- We have 4 possibilities for doing the junction JOIN here.+        -- This could be solved by specifying two additional fks, like whatev_projects!fk1!fk2(*)+        -- If the need arises this capability can be added later without causing a breaking change+        get "/whatev_sites?select=*,whatev_projects(*)" `shouldRespondWith`+          [json|+            {+              "details": [+                {+                  "cardinality": "many-to-many",+                  "relationship": "whatev_jobs using whatev_jobs_site_id_1_fkey(site_id_1) and whatev_jobs_project_id_1_fkey(project_id_1)",+                  "embedding": "whatev_sites with whatev_projects"+                },+                {+                  "cardinality": "many-to-many",+                  "relationship": "whatev_jobs using whatev_jobs_site_id_1_fkey(site_id_1) and whatev_jobs_project_id_2_fkey(project_id_2)",+                  "embedding": "whatev_sites with whatev_projects"+                },+                {+                  "cardinality": "many-to-many",+                  "relationship": "whatev_jobs using whatev_jobs_site_id_2_fkey(site_id_2) and whatev_jobs_project_id_1_fkey(project_id_1)",+                  "embedding": "whatev_sites with whatev_projects"+                },+                {+                  "cardinality": "many-to-many",+                  "relationship": "whatev_jobs using whatev_jobs_site_id_2_fkey(site_id_2) and whatev_jobs_project_id_2_fkey(project_id_2)",+                  "embedding": "whatev_sites with whatev_projects"+                }+              ],+              "hint": "Try changing 'whatev_projects' to one of the following: 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs'. Find the desired relationship in the 'details' key.",+              "message": "Could not embed because more than one relationship was found for 'whatev_sites' and 'whatev_projects'"+            }+          |]+          { matchStatus  = 300+          , matchHeaders = [matchContentTypeJson]+          }++    context "disambiguating requests with embed hints" $ do++      context "using FK to specify the relationship" $ do+        it "can embed by FK name" $+          get "/projects?id=in.(1,3)&select=id,name,client(id,name)" `shouldRespondWith`+            [json|[{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client":{"id":2,"name":"Apple"}}]|]+            { matchHeaders = [matchContentTypeJson] }++        it "can embed by FK name and select the FK column at the same time" $+          get "/projects?id=in.(1,3)&select=id,name,client_id,client(id,name)" `shouldRespondWith`+            [json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]+            { matchHeaders = [matchContentTypeJson] }++        it "can embed parent with view!fk and grandparent by using fk" $+          get "/tasks?id=eq.1&select=id,name,projects_view!project(id,name,client(id,name))" `shouldRespondWith`+            [json|[{"id":1,"name":"Design w7","projects_view":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]++        it "can embed by using a composite FK name" $+          get "/unit_workdays?select=unit_id,day,fst_shift(car_id,schedule(name)),snd_shift(camera_id,schedule(name))" `shouldRespondWith`+            [json| [+              {+                "day": "2019-12-02",+                "fst_shift": {+                    "car_id": "CAR-349",+                    "schedule": {+                        "name": "morning"+                    }+                },+                "snd_shift": {+                    "camera_id": "CAM-123",+                    "schedule": {+                        "name": "night"+                    }+                },+                "unit_id": 1+              }+            ] |]+            { matchHeaders = [matchContentTypeJson] }++        it "embeds by using two fks pointing to the same table" $+          get "/orders?id=eq.1&select=id, name, billing(address), shipping(address)" `shouldRespondWith`+            [json|[{"id":1,"name":"order 1","billing":{"address": "address 1"},"shipping":{"address": "address 2"}}]|]+            { matchHeaders = [matchContentTypeJson] }++        it "fails if the fk is not known" $+          get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`+            [json|{+              "hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",+              "message":"Could not find a relationship between message and person in the schema cache"}|]+            { matchStatus = 400+            , matchHeaders = [matchContentTypeJson] }++        it "can request a parent with fk" $+          get "/comments?select=content,user(name)" `shouldRespondWith`+            [json|[ { "content": "Needs to be delivered ASAP", "user": { "name": "Angela Martin" } } ]|]+            { matchHeaders = [matchContentTypeJson] }++        it "can request two parents with fks" $+          get "/articleStars?select=createdAt,article(id),user(name)&limit=1"+            `shouldRespondWith`+              [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"id": 1},"user":{"name": "Angela Martin"}}]|]++        it "can specify a view!fk" $+          get "/message?select=id,body,sender:person_detail!message_sender_fkey(name,sent),recipient:person_detail!message_recipient_fkey(name,received)&id=lt.4" `shouldRespondWith`+            [json|+              [{"id":1,"body":"Hello Jane","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}},+               {"id":2,"body":"Hi John","sender":{"name":"Jane","sent":1},"recipient":{"name":"John","received":1}},+               {"id":3,"body":"How are you doing?","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}}] |]+            { matchHeaders = [matchContentTypeJson] }++        it "can specify a table!fk hint and request children 2 levels" $+          get "/clients?id=eq.1&select=id,projects:projects!client(id,tasks(id))" `shouldRespondWith`+            [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]+            { matchHeaders = [matchContentTypeJson] }++        it "can disambiguate with the fk in case of an o2m and m2m relationship to the same table" $+          get "/sites?select=name,main_project(name)&site_id=eq.1" `shouldRespondWith`+            [json| [ { "name": "site 1", "main_project": { "name": "big project 1" } } ] |]+            { matchHeaders = [matchContentTypeJson] }++      context "using the column name of the FK to specify the relationship" $ do+        it "can embed by column" $+          get "/projects?id=in.(1,3)&select=id,name,client_id(id,name)" `shouldRespondWith`+            [json|[{"id":1,"name":"Windows 7","client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":{"id":2,"name":"Apple"}}]|]+            { matchHeaders = [matchContentTypeJson] }++        it "can embed by column and select the column at the same time, if aliased" $+          get "/projects?id=in.(1,3)&select=id,name,client_id,client:client_id(id,name)" `shouldRespondWith`+            [json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]+            { matchHeaders = [matchContentTypeJson] }++        it "can embed parent by using view!column and grandparent by using the column" $+          get "/tasks?id=eq.1&select=id,name,project:projects_view!project_id(id,name,client:client_id(id,name))" `shouldRespondWith`+            [json|[{"id":1,"name":"Design w7","project":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]++        it "can specify table!column" $+          get "/message?select=id,body,sender:person!sender(name),recipient:person!recipient(name)&id=lt.4" `shouldRespondWith`+            [json|+              [{"id":1,"body":"Hello Jane","sender":{"name":"John"},"recipient":{"name":"Jane"}},+               {"id":2,"body":"Hi John","sender":{"name":"Jane"},"recipient":{"name":"John"}},+               {"id":3,"body":"How are you doing?","sender":{"name":"John"},"recipient":{"name":"Jane"}}] |]+            { matchHeaders = [matchContentTypeJson] }++        it "will embed using a column that has uppercase chars" $+          get "/ghostBusters?select=escapeId(*)" `shouldRespondWith`+            [json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]+            { matchHeaders = [matchContentTypeJson] }++        it "embeds by using two columns pointing to the same table" $+          get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping_address_id(id)" `shouldRespondWith`+            [json|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping_address_id":{"id":2}}]|]+            { matchHeaders = [matchContentTypeJson] }++        it "can disambiguate with the column in case of an o2m and m2m relationship to the same table" $+          get "/sites?select=name,main_project_id(name)&site_id=eq.1" `shouldRespondWith`+            [json| [ { "name": "site 1", "main_project_id": { "name": "big project 1" } } ] |]+            { matchHeaders = [matchContentTypeJson] }++      context "using the junction to disambiguate the request" $+        it "can specify the junction of an m2m relationship" $ do+          get "/sites?select=*,big_projects!jobs(name)&site_id=in.(1,2)" `shouldRespondWith`+            [json|+              [+                {+                  "big_projects": [+                    {+                      "name": "big project 1"+                    }+                  ],+                  "main_project_id": 1,+                  "name": "site 1",+                  "site_id": 1+                },+                {+                  "big_projects": [+                    {+                      "name": "big project 1"+                    },+                    {+                      "name": "big project 2"+                    }+                  ],+                  "main_project_id": null,+                  "name": "site 2",+                  "site_id": 2+                }+              ]+            |]+          get "/sites?select=*,big_projects!main_jobs(name)&site_id=in.(1,2)" `shouldRespondWith`+            [json|+              [+                {+                  "big_projects": [+                    {+                      "name": "big project 1"+                    }+                  ],+                  "main_project_id": 1,+                  "name": "site 1",+                  "site_id": 1+                },+                {+                  "big_projects": [],+                  "main_project_id": null,+                  "name": "site 2",+                  "site_id": 2+                }+              ]+            |]+            { matchHeaders = [matchContentTypeJson] }++      context "using a FK column and a FK to specify the relationship" $+        it "embeds by using a column and a fk pointing to the same table" $+          get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping(id)" `shouldRespondWith`+            [json|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping":{"id":2}}]|]+            { matchHeaders = [matchContentTypeJson] }++    context "tables with self reference foreign keys" $ do+      context "one self reference foreign key" $ do+        it "embeds parents recursively" $+          get "/family_tree?id=in.(3,4)&select=id,parent(id,name,parent(*))" `shouldRespondWith`+            [json|[+              { "id": "3", "parent": { "id": "1", "name": "Parental Unit", "parent": null } },+              { "id": "4", "parent": { "id": "2", "name": "Kid One", "parent": { "id": "1", "name": "Parental Unit", "parent": null } } }+            ]|]+            { matchHeaders = [matchContentTypeJson] }++        it "embeds children recursively" $+          get "/family_tree?id=eq.1&select=id,name, children:family_tree!parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`+            [json|[{+              "id": "1", "name": "Parental Unit", "children": [+                { "id": "2", "name": "Kid One", "children": [ { "id": "4", "name": "Grandkid One" } ] },+                { "id": "3", "name": "Kid Two", "children": [ { "id": "5", "name": "Grandkid Two" } ] }+              ]+            }]|] { matchHeaders = [matchContentTypeJson] }++        it "embeds parent and then embeds children" $+          get "/family_tree?id=eq.2&select=id,name,parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`+            [json|[{+              "id": "2", "name": "Kid One", "parent": {+                "id": "1", "name": "Parental Unit", "children": [ { "id": "2", "name": "Kid One" }, { "id": "3", "name": "Kid Two"} ]+              }+            }]|] { matchHeaders = [matchContentTypeJson] }++      context "two self reference foreign keys" $ do+        it "embeds parents" $+          get "/organizations?select=id,name,referee(id,name),auditor(id,name)&id=eq.3" `shouldRespondWith`+            [json|[{+              "id": 3, "name": "Acme",+              "referee": {+                "id": 1,+                "name": "Referee Org"+              },+              "auditor": {+                "id": 2,+                "name": "Auditor Org"+              }+            }]|] { matchHeaders = [matchContentTypeJson] }++        it "embeds children" $ do+          get "/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1" `shouldRespondWith`+            [json|[{+              "id": 1, "name": "Referee Org",+              "refereeds": [+                {+                  "id": 3,+                  "name": "Acme"+                },+                {+                  "id": 4,+                  "name": "Umbrella"+                }+              ]+            }]|] { matchHeaders = [matchContentTypeJson] }+          get "/organizations?select=id,name,auditees:organizations!auditor(id,name)&id=eq.2" `shouldRespondWith`+            [json|[{+              "id": 2, "name": "Auditor Org",+              "auditees": [+                {+                  "id": 3,+                  "name": "Acme"+                },+                {+                  "id": 4,+                  "name": "Umbrella"+                }+              ]+            }]|] { matchHeaders = [matchContentTypeJson] }++        it "embeds other relations(manager) besides the self reference" $ do+          get "/organizations?select=name,manager(name),referee(name,manager(name),auditor(name,manager(name))),auditor(name,manager(name),referee(name,manager(name)))&id=eq.5" `shouldRespondWith`+            [json|[{+              "name":"Cyberdyne",+              "manager":{"name":"Cyberdyne Manager"},+              "referee":{+                "name":"Acme",+                "manager":{"name":"Acme Manager"},+                "auditor":{+                  "name":"Auditor Org",+                  "manager":{"name":"Auditor Manager"}}},+              "auditor":{+                "name":"Umbrella",+                "manager":{"name":"Umbrella Manager"},+                "referee":{+                  "name":"Referee Org",+                  "manager":{"name":"Referee Manager"}}}+            }]|] { matchHeaders = [matchContentTypeJson] }++          get "/organizations?select=name,manager(name),auditees:organizations!auditor(name,manager(name),refereeds:organizations!referee(name,manager(name)))&id=eq.2" `shouldRespondWith`+            [json|[{+              "name":"Auditor Org",+              "manager":{"name":"Auditor Manager"},+              "auditees":[+                {"name":"Acme",+                 "manager":{"name":"Acme Manager"},+                 "refereeds":[+                   {"name":"Cyberdyne",+                    "manager":{"name":"Cyberdyne Manager"}},+                   {"name":"Oscorp",+                    "manager":{"name":"Oscorp Manager"}}]},+                {"name":"Umbrella",+                 "manager":{"name":"Umbrella Manager"},+                 "refereeds":[]}]+            }]|] { matchHeaders = [matchContentTypeJson] }++    context "m2m embed when there's a junction in an internal schema" $ do+      -- https://github.com/PostgREST/postgrest/issues/1736+      it "works with no ambiguity when there's an exposed view of the junction" $ do+        get "/screens?select=labels(name)" `shouldRespondWith`+          [json|[{"labels":[{"name":"fruit"}]}, {"labels":[{"name":"vehicles"}]}, {"labels":[{"name":"vehicles"}, {"name":"fruit"}]}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/actors?select=*,films(*)" `shouldRespondWith`+          [json|[ {"id":1,"name":"john","films":[{"id":12,"title":"douze commandements"}]},+                  {"id":2,"name":"mary","films":[{"id":2001,"title":"odyssée de l'espace"}]}]|]+          { matchHeaders = [matchContentTypeJson] }+      it "doesn't work if the junction is only internal" $+        get "/end_1?select=end_2(*)" `shouldRespondWith`+          [json|{+            "hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",+            "message":"Could not find a relationship between end_1 and end_2 in the schema cache"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson] }+      it "shouldn't try to embed if the private junction has an exposed homonym" $+        -- ensures the "invalid reference to FROM-clause entry for table "rollen" error doesn't happen.+        -- Ref: https://github.com/PostgREST/postgrest/issues/1587#issuecomment-734995669+        get "/schauspieler?select=filme(*)" `shouldRespondWith`+          [json|{+            "hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",+            "message":"Could not find a relationship between schauspieler and filme in the schema cache"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson] }
+ test/spec/Feature/EmbedInnerJoinSpec.hs view
@@ -0,0 +1,356 @@+module Feature.EmbedInnerJoinSpec where++import Network.HTTP.Types+import Network.Wai        (Application)++import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude  hiding (get)+import SpecHelper++spec :: SpecWith ((), Application)+spec =+  describe "Embedding with an inner join" $ do+    context "many-to-one relationships" $ do+      it "ignores null embeddings while the default left join doesn't" $ do+        get "/projects?select=id,clients!inner(id)" `shouldRespondWith`+          [json|[+            {"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}},+            {"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/projects?select=id,clients!left(id)" `shouldRespondWith`+          [json|[+            {"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}},+            {"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}},+            {"id":5,"clients":null}]|]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/projects?select=id,clients!inner(id)" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-3/4" ]+          }++      it "filters source tables when the embedded table is filtered" $ do+        get "/projects?select=id,clients!inner(id)&clients.id=eq.1" `shouldRespondWith`+          [json|[+            {"id":1,"clients":{"id":1}},+            {"id":2,"clients":{"id":1}}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/projects?select=id,clients!inner(id)&clients.id=eq.2" `shouldRespondWith`+          [json|[+            {"id":3,"clients":{"id":2}},+            {"id":4,"clients":{"id":2}}] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/projects?select=id,clients!inner(id)&clients.id=eq.0" `shouldRespondWith`+          [json|[]|]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/projects?select=id,clients!inner(id)&clients.id=eq.1" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-1/2" ]+          }++      it "filters source tables when a two levels below embedded table is filtered" $ do+        get "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.1" `shouldRespondWith`+          [json|[+            {"id":1,"projects":{"id":1,"clients":{"id":1}}},+            {"id":2,"projects":{"id":1,"clients":{"id":1}}},+            {"id":3,"projects":{"id":2,"clients":{"id":1}}},+            {"id":4,"projects":{"id":2,"clients":{"id":1}}}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.2" `shouldRespondWith`+          [json|[+            {"id":5,"projects":{"id":3,"clients":{"id":2}}},+            {"id":6,"projects":{"id":3,"clients":{"id":2}}},+            {"id":7,"projects":{"id":4,"clients":{"id":2}}},+            {"id":8,"projects":{"id":4,"clients":{"id":2}}}]|]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.1" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-3/4" ]+          }++      it "only affects the source table rows if his direct embedding is an inner join" $ do+        get "/tasks?select=id,projects(id,clients!inner(id))&projects.clients.id=eq.2" `shouldRespondWith`+          [json|[+            {"id":1,"projects":null},+            {"id":2,"projects":null},+            {"id":3,"projects":null},+            {"id":4,"projects":null},+            {"id":5,"projects":{"id":3,"clients":{"id":2}}},+            {"id":6,"projects":{"id":3,"clients":{"id":2}}},+            {"id":7,"projects":{"id":4,"clients":{"id":2}}},+            {"id":8,"projects":{"id":4,"clients":{"id":2}}}]|]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/tasks?select=id,projects(id,clients!inner(id))&projects.clients.id=eq.2" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-7/8" ]+          }++      it "works with views" $ do+        get "/books?select=title,authors!inner(name)&authors.name=eq.George%20Orwell" `shouldRespondWith`+          [json| [{"title":"1984","authors":{"name":"George Orwell"}}] |]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/books?select=title,authors!inner(name)&authors.name=eq.George%20Orwell" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-0/1" ]+          }++    context "one-to-many relationships" $ do+      it "ignores empty array embeddings while the default left join doesn't" $ do+        get "/entities?select=id,child_entities!inner(id)" `shouldRespondWith`+          [json|[+            {"id":1,"child_entities":[{"id":1}, {"id":2}, {"id":4}, {"id":5}]},+            {"id":2,"child_entities":[{"id":3}, {"id":6}]}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/entities?select=id,child_entities!left(id)" `shouldRespondWith`+          [json| [+            {"id":1,"child_entities":[{"id":1}, {"id":2}, {"id":4}, {"id":5}]},+            {"id":2,"child_entities":[{"id":3}, {"id":6}]},+            {"id":3,"child_entities":[]},+            {"id":4,"child_entities":[]}] |]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/entities?select=id,child_entities!inner(id)" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-1/2" ]+          }++      it "filters source tables when the embedded table is filtered" $ do+        get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.1" `shouldRespondWith`+          [json|[{"id":1,"child_entities":[{"id":1}]}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.3" `shouldRespondWith`+          [json|[{"id":2,"child_entities":[{"id":3}]}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.0" `shouldRespondWith`+          [json|[]|]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.1" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-0/1" ]+          }++      it "filters source tables when a two levels below embedded table is filtered" $ do+        get "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=in.(1,5)"+          `shouldRespondWith`+          [json|[+            {+              "id": 1,+              "child_entities": [+                { "id": 1, "grandchild_entities": [ { "id": 1 } ] },+                { "id": 2, "grandchild_entities": [ { "id": 5 } ] }]+            }+          ]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=eq.2" `shouldRespondWith`+          [json|[+            {+              "id": 1,+              "child_entities": [+                { "id": 1, "grandchild_entities": [ { "id": 2 } ] } ]+            }+          ]|]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=in.(1,5)" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-0/1" ]+          }++      it "only affects the source table rows if his direct embedding is an inner join" $ do+        get "/entities?select=id,child_entities!inner(id,grandchild_entities(id))&child_entities.grandchild_entities.id=eq.2" `shouldRespondWith`+          [json|[+            {+              "id": 1,+              "child_entities": [+                { "id": 1, "grandchild_entities": [ { "id": 2 } ] },+                { "id": 2, "grandchild_entities": [] },+                { "id": 4, "grandchild_entities": [] },+                { "id": 5, "grandchild_entities": [] } ]+            },+            {+              "id": 2,+              "child_entities": [+                { "id": 3, "grandchild_entities": [] },+                { "id": 6, "grandchild_entities": [] } ]+            }+          ]|]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/entities?select=id,child_entities!inner(id,grandchild_entities(id))&child_entities.grandchild_entities.id=eq.2" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-1/2" ]+          }++      it "works with views" $ do+        get "/authors?select=*,books!inner(*)&books.title=eq.1984" `shouldRespondWith`+          [json| [{"id":1,"name":"George Orwell","books":[{"id":1,"title":"1984","publication_year":1949,"author_id":1}]}] |]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/authors?select=*,books!inner(*)&books.title=eq.1984" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-0/1" ]+          }++    context "many-to-many relationships" $ do+      it "ignores empty array embeddings while the default left join doesn't" $ do+        get "/products?select=id,suppliers!inner(id)" `shouldRespondWith`+          [json| [+            {"id":1,"suppliers":[{"id":1}, {"id":2}]},+            {"id":2,"suppliers":[{"id":1}, {"id":3}]}] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/products?select=id,suppliers!left(id)" `shouldRespondWith`+          [json| [+            {"id":1,"suppliers":[{"id":1}, {"id":2}]},+            {"id":2,"suppliers":[{"id":1}, {"id":3}]},+            {"id":3,"suppliers":[]}] |]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/products?select=id,suppliers!inner(id)" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-1/2" ]+          }++      it "filters source tables when the embedded table is filtered" $ do+        get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.2" `shouldRespondWith`+          [json| [{"id":1,"suppliers":[{"id":2}]}] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.3" `shouldRespondWith`+          [json| [{"id":2,"suppliers":[{"id":3}]}] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.0" `shouldRespondWith`+          [json| [] |]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/products?select=id,suppliers!inner(id)&suppliers.id=eq.2" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-0/1" ]+          }++      it "filters source tables when a two levels below embedded table is filtered" $ do+        get "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.3"+          `shouldRespondWith`+          [json|[{"id":1,"suppliers":[{"id":2,"trade_unions":[{"id":3}]}]}] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.4"+          `shouldRespondWith`+          [json|[{"id":1,"suppliers":[{"id":2,"trade_unions":[{"id":4}]}]}] |]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.3" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-0/1" ]+          }++      it "only affects the source table rows if his direct embedding is an inner join" $ do+        get "/products?select=id,suppliers!inner(id,trade_unions(id))&suppliers.trade_unions.id=eq.3" `shouldRespondWith`+          [json|[+            {"id":1,"suppliers":[{"id":1,"trade_unions":[]}, {"id":2,"trade_unions":[{"id":3}]}]},+            {"id":2,"suppliers":[{"id":1,"trade_unions":[]}, {"id":3,"trade_unions":[]}]}]|]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/products?select=id,suppliers!inner(id,trade_unions(id))&suppliers.trade_unions.id=eq.3" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-1/2" ]+          }++      it "works with views" $ do+        get "/actors?select=*,films!inner(*)&films.title=eq.douze%20commandements" `shouldRespondWith`+          [json| [{"id":1,"name":"john","films":[{"id":12,"title":"douze commandements"}]}] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/films?select=*,actors!inner(*)&actors.name=eq.john" `shouldRespondWith`+          [json| [{"id":12,"title":"douze commandements","actors":[{"id":1,"name":"john"}]}] |]+          { matchHeaders = [matchContentTypeJson] }+        request methodHead "/actors?select=*,films!inner(*)&films.title=eq.douze%20commandements" [("Prefer", "count=exact")] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson+                           , "Content-Range" <:> "0-0/1" ]+          }++    it "works with m2o and m2m relationships combined" $ do+      get "/projects?select=name,clients!inner(name),users!inner(name)" `shouldRespondWith`+        [json| [+          {"name":"Windows 7","clients":{"name":"Microsoft"},"users":[{"name":"Angela Martin"}, {"name":"Dwight Schrute"}]},+          {"name":"Windows 10","clients":{"name":"Microsoft"},"users":[{"name":"Angela Martin"}]},+          {"name":"IOS","clients":{"name":"Apple"},"users":[{"name":"Michael Scott"}, {"name":"Dwight Schrute"}]},+          {"name":"OSX","clients":{"name":"Apple"},"users":[{"name":"Michael Scott"}]}]|]+        { matchHeaders = [matchContentTypeJson] }+      request methodHead "/projects?select=name,clients!inner(name),users!inner(name)" [("Prefer", "count=exact")] mempty+        `shouldRespondWith` ""+        { matchStatus  = 200+        , matchHeaders = [ matchContentTypeJson+                         , "Content-Range" <:> "0-3/4" ]+        }++    it "works with rpc" $ do+      get "/rpc/getallprojects?select=id,clients!inner(id)&clients.id=eq.1" `shouldRespondWith`+        [json| [{"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}}] |]+        { matchHeaders = [matchContentTypeJson] }+      request methodHead "/rpc/getallprojects?select=id,clients!inner(id)&clients.id=eq.1" [("Prefer", "count=exact")] mempty+        `shouldRespondWith` ""+        { matchStatus  = 200+        , matchHeaders = [ matchContentTypeJson+                         , "Content-Range" <:> "0-1/2" ]+        }++    it "works when using hints" $ do+      get "/projects?select=id,clients!client!inner(id)&clients.id=eq.2" `shouldRespondWith`+        [json| [{"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}}] |]+        { matchHeaders = [matchContentTypeJson] }+      get "/projects?select=id,client!inner(id)&client.id=eq.2" `shouldRespondWith`+        [json| [{"id":3,"client":{"id":2}}, {"id":4,"client":{"id":2}}] |]+        { matchHeaders = [matchContentTypeJson] }+      request methodHead "/projects?select=id,clients!client!inner(id)&clients.id=eq.2" [("Prefer", "count=exact")] mempty+        `shouldRespondWith` ""+        { matchStatus  = 200+        , matchHeaders = [ matchContentTypeJson+                         , "Content-Range" <:> "0-1/2" ]+        }++    it "works with many one-to-many relationships" $ do+      -- https://github.com/PostgREST/postgrest/issues/1977+      get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)" `shouldRespondWith`+        [json|[+          {"id":1,"name":"Walmart","contact":[{"name":"Wally Walton"}, {"name":"Wilma Wellers"}],"clientinfo":[{"other":"123 Main St"}]},+          {"id":2,"name":"Target", "contact":[{"name":"Tabby Targo"}],"clientinfo":[{"other":"456 South 3rd St"}]},+          {"id":3,"name":"Big Lots","contact":[{"name":"Bobby Bots"}, {"name":"Bonnie Bits"}, {"name":"Billy Boats"}],"clientinfo":[{"other":"789 Palm Tree Ln"}]}+        ]|]+        { matchHeaders = [matchContentTypeJson] }+      get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)&contact.name=eq.Wally%20Walton" `shouldRespondWith`+        [json|[+          {"id":1,"name":"Walmart","contact":[{"name":"Wally Walton"}],"clientinfo":[{"other":"123 Main St"}]}+        ]|]+        { matchHeaders = [matchContentTypeJson] }+      get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)&clientinfo.other=eq.456%20South%203rd%20St" `shouldRespondWith`+        [json|[+          {"id":2,"name":"Target","clientinfo":[{"other":"456 South 3rd St"}],"contact":[{"name":"Tabby Targo"}]}+        ]|]+        { matchHeaders = [matchContentTypeJson] }+      request methodHead "/client?select=id,name,contact!inner(name),clientinfo!inner(other)" [("Prefer", "count=exact")] mempty+        `shouldRespondWith` ""+        { matchStatus  = 200+        , matchHeaders = [ matchContentTypeJson+                         , "Content-Range" <:> "0-2/3" ]+        }
+ test/spec/Feature/ExtraSearchPathSpec.hs view
@@ -0,0 +1,39 @@+module Feature.ExtraSearchPathSpec where++import Network.HTTP.Types+import Network.Wai         (Application)+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude  hiding (get)+import SpecHelper++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] }++  it "can detect fk relations through multiple views recursively when middle views are in extra search path" $+    get "/consumers_extra_view?select=*,orders_view(*)" `shouldRespondWith` 200
+ test/spec/Feature/HtmlRawOutputSpec.hs view
@@ -0,0 +1,30 @@+module Feature.HtmlRawOutputSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec         hiding (pendingWith)+import Test.Hspec.Wai+import Text.Heredoc++import Protolude  hiding (get)+import SpecHelper (acceptHdrs)++spec :: SpecWith ((), Application)+spec = describe "When raw-media-types is set to \"text/html\"" $+  it "can get raw output with Accept: text/html" $+    request methodGet "/rpc/welcome.html" (acceptHdrs "text/html") ""+      `shouldRespondWith`+      [str|+          |<html>+          |  <head>+          |    <title>PostgREST</title>+          |  </head>+          |  <body>+          |    <h1>Welcome to PostgREST</h1>+          |  </body>+          |</html>+          |]+      { matchStatus = 200+      , matchHeaders = ["Content-Type" <:> "text/html"]+      }
+ test/spec/Feature/IgnorePrivOpenApiSpec.hs view
@@ -0,0 +1,76 @@+module Feature.IgnorePrivOpenApiSpec where++import Control.Lens ((^?))++import Data.Aeson.Lens+import Data.Aeson.QQ++import Network.HTTP.Types+import Network.Wai        (Application)+import Network.Wai.Test   (SResponse (..))++import Test.Hspec     hiding (pendingWith)+import Test.Hspec.Wai++import Protolude  hiding (get)+import SpecHelper++spec :: SpecWith ((), Application)+spec = describe "OpenAPI Ignore Privileges" $ do+  it "root path returns a valid openapi spec" $ do+    validateOpenApiResponse [("Accept", "application/openapi+json")]+    request methodHead "/" (acceptHdrs "application/openapi+json") ""+      `shouldRespondWith` "" { matchStatus  = 200 }++  describe "table" $ do++    it "includes privileged table even if user does not have permission" $ do+      r <- simpleBody <$> get "/"+      let tableTag = r ^? key "paths" . key "/authors_only"+                     . key "post" . key "tags"+                     . nth 0++      liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|]++    it "only includes tables that belong to another schema if the Accept-Profile header is used" $ do+      r1 <- simpleBody <$> get "/"+      let tableKey1 = r1 ^? key "paths" . key "/children"++      liftIO $ tableKey1 `shouldBe` Nothing++      r2 <- simpleBody <$> request methodGet "/" [("Accept-Profile", "v1")] ""+      let tableKey2 = r2 ^? key "paths" . key "/children"++      liftIO $ tableKey2 `shouldNotBe` Nothing++    it "includes comments on tables" $ do+      r <- simpleBody <$> get "/"++      let grandChildGet s = key "paths" . key "/grandchild_entities" . key "get" . key s+          grandChildGetSummary = r ^? grandChildGet "summary"+          grandChildGetDescription = r ^? grandChildGet "description"++      liftIO $ do+        grandChildGetSummary `shouldBe` Just "grandchild_entities summary"+        grandChildGetDescription `shouldBe` Just "grandchild_entities description\nthat spans\nmultiple lines"++  describe "RPC" $ do++    it "includes privileged function even if user does not have permission" $ do+      r <- simpleBody <$> get "/"+      let funcTag = r ^? key "paths" . key "/rpc/privileged_hello"+                    . key "post" . key "tags"+                    . nth 0++      liftIO $ funcTag `shouldBe` Just [aesonQQ|"(rpc) privileged_hello"|]++    it "only includes functions that belong to another schema if the Accept-Profile header is used" $ do+      r1 <- simpleBody <$> get "/"+      let funcKey1 = r1 ^? key "paths" . key "/rpc/get_parents_below"++      liftIO $ funcKey1 `shouldBe` Nothing++      r2 <- simpleBody <$> request methodGet "/" [("Accept-Profile", "v1")] ""+      let funcKey2 = r2 ^? key "paths" . key "/rpc/get_parents_below"++      liftIO $ funcKey2 `shouldNotBe` Nothing
+ test/spec/Feature/InsertSpec.hs view
@@ -0,0 +1,558 @@+module Feature.InsertSpec where++import Data.List              (lookup)+import Network.Wai            (Application)+import Network.Wai.Test       (SResponse (simpleHeaders))+import Test.Hspec             hiding (pendingWith)+import Test.Hspec.Wai.Matcher (bodyEquals)++import Network.HTTP.Types+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON+import Text.Heredoc++import PostgREST.Config.PgVersion (PgVersion, pgVersion110,+                                   pgVersion112, pgVersion130)++import Protolude  hiding (get)+import SpecHelper++spec :: PgVersion -> SpecWith ((), Application)+spec actualPgVersion = do+  describe "Posting new record" $ do+    context "disparate json types" $ do+      it "accepts disparate json types" $ do+        post "/menagerie"+          [json| {+            "integer": 13, "double": 3.14159, "varchar": "testing!"+          , "boolean": false, "date": "1900-01-01", "money": "$3.99"+          , "enum": "foo"+          } |] `shouldRespondWith` ""+          { matchStatus  = 201+            -- should not have content type set when body is empty+          , matchHeaders = [matchHeaderAbsent hContentType]+          }++      it "filters columns in result using &select" $+        request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=representation")]+          [json| [{+            "integer": 14, "double": 3.14159, "varchar": "testing!"+          , "boolean": false, "date": "1900-01-01", "money": "$3.99"+          , "enum": "foo"+          }] |] `shouldRespondWith` [json|[{"integer":14,"varchar":"testing!"}]|]+          { matchStatus  = 201+          , matchHeaders = [matchContentTypeJson]+          }++      it "ignores &select when return not set or using return=minimal" $ do+        request methodPost "/menagerie?select=integer,varchar" []+          [json| [{+            "integer": 15, "double": 3.14159, "varchar": "testing!"+          , "boolean": false, "date": "1900-01-01", "money": "$3.99"+          , "enum": "foo"+          }] |] `shouldRespondWith` ""+          { matchStatus  = 201 }+        request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=minimal")]+          [json| [{+            "integer": 16, "double": 3.14159, "varchar": "testing!"+          , "boolean": false, "date": "1900-01-01", "money": "$3.99"+          , "enum": "foo"+          }] |] `shouldRespondWith` ""+          { matchStatus  = 201 }++    context "non uniform json array" $ do+      it "rejects json array that isn't exclusivily composed of objects" $+        post "/articles"+             [json| [{"id": 100, "body": "xxxxx"}, 123, "xxxx", {"id": 111, "body": "xxxx"}] |]+        `shouldRespondWith`+             [json| {"message":"All object keys must match"} |]+             { matchStatus  = 400+             , matchHeaders = [matchContentTypeJson]+             }++      it "rejects json array that has objects with different keys" $+        post "/articles"+             [json| [{"id": 100, "body": "xxxxx"}, {"id": 111, "body": "xxxx", "owner": "me"}] |]+        `shouldRespondWith`+             [json| {"message":"All object keys must match"} |]+             { matchStatus  = 400+             , matchHeaders = [matchContentTypeJson]+             }++    context "requesting full representation" $ do+      it "includes related data after insert" $+        request methodPost "/projects?select=id,name,clients(id,name)"+                [("Prefer", "return=representation"), ("Prefer", "count=exact")]+          [json|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` [json|[{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}]|]+          { matchStatus  = 201+          , matchHeaders = [ matchContentTypeJson+                           , "Location" <:> "/projects?id=eq.6"+                           , "Content-Range" <:> "*/1" ]+          }++      it "can rename and cast the selected columns" $+        request methodPost "/projects?select=pId:id::text,pName:name,cId:client_id::text"+                [("Prefer", "return=representation")]+          [json|{"id":7,"name":"New Project","client_id":2}|] `shouldRespondWith`+          [json|[{"pId":"7","pName":"New Project","cId":"2"}]|]+          { matchStatus  = 201+          , matchHeaders = [ matchContentTypeJson+                           , "Location" <:> "/projects?id=eq.7"+                           , "Content-Range" <:> "*/*" ]+          }++      it "should not throw and return location header when selecting without PK" $+        request methodPost "/projects?select=name,client_id" [("Prefer", "return=representation")]+          [json|{"id":10,"name":"New Project","client_id":2}|] `shouldRespondWith`+          [json|[{"name":"New Project","client_id":2}]|]+          { matchStatus  = 201+          , matchHeaders = [ matchContentTypeJson+                           , "Location" <:> "/projects?id=eq.10"+                           , "Content-Range" <:> "*/*" ]+          }++    context "requesting headers only representation" $ do+      it "should not throw and return location header when selecting without PK" $+        request methodPost "/projects?select=name,client_id" [("Prefer", "return=headers-only")]+          [json|{"id":11,"name":"New Project","client_id":2}|] `shouldRespondWith` ""+          { matchStatus  = 201+          , matchHeaders = [ "Location" <:> "/projects?id=eq.11"+                           , "Content-Range" <:> "*/*" ]+          }++      when (actualPgVersion >= pgVersion110) $+        it "should not throw and return location header for partitioned tables when selecting without PK" $+          request methodPost "/car_models" [("Prefer", "return=headers-only")]+            [json|{"name":"Enzo","year":2021}|] `shouldRespondWith` ""+            { matchStatus  = 201+            , matchHeaders = [ "Location" <:> "/car_models?name=eq.Enzo&year=eq.2021"+                             , "Content-Range" <:> "*/*" ]+            }++    context "requesting no representation" $+      it "should not throw and return no location header when selecting without PK" $+        request methodPost "/projects?select=name,client_id" []+          [json|{"id":12,"name":"New Project","client_id":2}|] `shouldRespondWith` ""+          { matchStatus  = 201+          , matchHeaders = [ matchHeaderAbsent hLocation ]+          }++    context "from an html form" $+      it "accepts disparate json types" $ do+        request methodPost "/menagerie"+            [("Content-Type", "application/x-www-form-urlencoded")]+             ("integer=7&double=2.71828&varchar=forms+are+fun&" <>+              "boolean=false&date=1900-01-01&money=$3.99&enum=foo")+          `shouldRespondWith`+            ""+            { matchStatus = 201 }++    context "with no pk supplied" $ do+      context "into a table with auto-incrementing pk" $+        it "succeeds with 201 and location header" $ do+          -- reset pk sequence first to make test repeatable+          request methodPost "/rpc/reset_sequence"+              [("Prefer", "tx=commit")]+              [json|{"name": "auto_incrementing_pk_id_seq", "value": 2}|]+            `shouldRespondWith`+              [json|""|]++          request methodPost "/auto_incrementing_pk" [("Prefer", "return=headers-only")]+              [json| { "non_nullable_string":"not null"} |]+            `shouldRespondWith`+              ""+              { matchStatus  = 201+              , matchHeaders = [ "Location" <:> "/auto_incrementing_pk?id=eq.2" ]+              }++      context "into a table with simple pk" $+        it "fails with 400 and error" $+          post "/simple_pk" [json| { "extra":"foo"} |]+          `shouldRespondWith`+          (if actualPgVersion >= pgVersion130 then+            [json|{"hint":null,"details":"Failing row contains (null, foo).","code":"23502","message":"null value in column \"k\" of relation \"simple_pk\" violates not-null constraint"}|]+           else+            [json|{"hint":null,"details":"Failing row contains (null, foo).","code":"23502","message":"null value in column \"k\" violates not-null constraint"}|]+          )+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson]+          }++      context "into a table with no pk" $ do+        it "succeeds with 201 but no location header" $ do+          post "/no_pk" [json| { "a":"foo", "b":"bar" } |]+            `shouldRespondWith`+              ""+              { matchStatus  = 201+              , matchHeaders = [matchHeaderAbsent hLocation]+              }++        it "returns full details of inserted record if asked" $ do+          request methodPost "/no_pk"+              [("Prefer", "return=representation")]+              [json| { "a":"bar", "b":"baz" } |]+            `shouldRespondWith`+              [json| [{ "a":"bar", "b":"baz" }] |]+              { matchStatus  = 201+              , matchHeaders = [matchHeaderAbsent hLocation]+              }++        it "returns empty array when no items inserted, and return=rep" $ do+          request methodPost "/no_pk"+              [("Prefer", "return=representation")]+              [json| [] |]+            `shouldRespondWith`+              [json| [] |]+              { matchStatus = 201 }++        it "can post nulls" $ do+          request methodPost "/no_pk"+              [("Prefer", "return=representation")]+              [json| { "a":null, "b":"foo" } |]+            `shouldRespondWith`+              [json| [{ "a":null, "b":"foo" }] |]+              { matchStatus  = 201+              , matchHeaders = [matchHeaderAbsent hLocation]+              }++    context "with compound pk supplied" $+      it "builds response location header appropriately" $ do+        request methodPost "/compound_pk"+            [("Prefer", "return=representation")]+            [json| { "k1":12, "k2":"Rock & R+ll" } |]+          `shouldRespondWith`+            [json|[ { "k1":12, "k2":"Rock & R+ll", "extra": null } ]|]+            { matchStatus  = 201+            , matchHeaders = [ "Location" <:> "/compound_pk?k1=eq.12&k2=eq.Rock%20%26%20R%2Bll" ]+            }++    context "with bulk insert" $+      it "returns 201 but no location header" $ do+        let bulkData = [json| [ {"k1":21, "k2":"hello world"}+                              , {"k1":22, "k2":"bye for now"}]+                            |]+        request methodPost "/compound_pk" [] bulkData+          `shouldRespondWith`+            ""+            { matchStatus  = 201+            , matchHeaders = [matchHeaderAbsent hLocation]+            }++    context "with invalid json payload" $+      it "fails with 400 and error" $+        post "/simple_pk" "}{ x = 2"+        `shouldRespondWith`+        [json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]+        { matchStatus  = 400+        , matchHeaders = [matchContentTypeJson]+        }++    context "with no payload" $+      it "fails with 400 and error" $+        post "/simple_pk" ""+        `shouldRespondWith`+        [json|{"message":"Error in $: not enough input"}|]+        { matchStatus  = 400+        , matchHeaders = [matchContentTypeJson]+        }++    context "with valid json payload" $+      it "succeeds and returns 201 created" $+        post "/simple_pk" [json| { "k":"k1", "extra":"e1" } |] `shouldRespondWith` 201++    context "attempting to insert a row with the same primary key" $+      it "fails returning a 409 Conflict" $+        post "/simple_pk"+            [json| { "k":"xyyx", "extra":"e1" } |]+          `shouldRespondWith`+            [json|{"hint":null,"details":"Key (k)=(xyyx) already exists.","code":"23505","message":"duplicate key value violates unique constraint \"simple_pk_pkey\""}|]+            { matchStatus  = 409 }++    context "attempting to insert a row with conflicting unique constraint" $+      it "fails returning a 409 Conflict" $+        post "/withUnique"  [json| { "uni":"nodup", "extra":"e2" } |] `shouldRespondWith` 409++    context "jsonb" $ do+      it "serializes nested object" $ do+        let inserted = [json| { "data": { "foo":"bar" } } |]+        request methodPost "/json_table"+                     [("Prefer", "return=representation")]+                     inserted+          `shouldRespondWith` [json|[{"data":{"foo":"bar"}}]|]+          { matchStatus  = 201+          }++      it "serializes nested array" $ do+        let inserted = [json| { "data": [1,2,3] } |]+        request methodPost "/json_table"+                     [("Prefer", "return=representation")]+                     inserted+          `shouldRespondWith` [json|[{"data":[1,2,3]}]|]+          { matchStatus  = 201+          }++    context "empty objects" $ do+      it "successfully inserts a row with all-default columns" $ do+        post "/items" "{}" `shouldRespondWith` ""+          { matchStatus  = 201+          , matchHeaders = []+          }+        post "/items" "[{}]" `shouldRespondWith` ""+          { matchStatus  = 201+          , matchHeaders = []+          }++      it "successfully inserts two rows with all-default columns" $+        post "/items" "[{}, {}]" `shouldRespondWith` ""+          { matchStatus  = 201+          , matchHeaders = []+          }++      it "successfully inserts a row with all-default columns with prefer=rep" $ do+        -- reset pk sequence first to make test repeatable+        request methodPost "/rpc/reset_sequence"+            [("Prefer", "tx=commit")]+            [json|{"name": "items2_id_seq", "value": 20}|]+          `shouldRespondWith`+            [json|""|]++        request methodPost "/items2"+            [("Prefer", "return=representation")]+            [json|{}|]+          `shouldRespondWith`+            [json|[{ id: 20 }]|]+            { matchStatus  = 201 }++      it "successfully inserts a row with all-default columns with prefer=rep and &select=" $ do+        -- reset pk sequence first to make test repeatable+        request methodPost "/rpc/reset_sequence"+            [("Prefer", "tx=commit")]+            [json|{"name": "items3_id_seq", "value": 20}|]+          `shouldRespondWith`+            [json|""|]++        request methodPost "/items3?select=id"+            [("Prefer", "return=representation")]+            [json|{}|]+          `shouldRespondWith` [json|[{ id: 20 }]|]+            { matchStatus  = 201 }++    context "POST with ?columns parameter" $ do+      it "ignores json keys not included in ?columns" $ do+        request methodPost "/articles?columns=id,body" [("Prefer", "return=representation")]+          [json| {"id": 200, "body": "xxx", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith`+          [json|[{"id": 200, "body": "xxx", "owner": "postgrest_test_anonymous"}]|]+          { matchStatus  = 201+          , matchHeaders = [] }+        request methodPost "/articles?columns=id,body&select=id,body" [("Prefer", "return=representation")]+          [json| [+            {"id": 201, "body": "yyy", "smth": "here", "other": "stuff", "fake_id": 13},+            {"id": 202, "body": "zzz", "garbage": "%%$&", "kkk": "jjj"},+            {"id": 203, "body": "aaa", "hey": "ho"} ]|] `shouldRespondWith`+          [json|[+            {"id": 201, "body": "yyy"},+            {"id": 202, "body": "zzz"},+            {"id": 203, "body": "aaa"} ]|]+          { matchStatus  = 201+          , matchHeaders = [] }++      -- TODO parse columns error message needs to be improved+      it "disallows blank ?columns" $+        post "/articles?columns="+          [json|[+            {"id": 204, "body": "yyy"},+            {"id": 205, "body": "zzz"}]|]+          `shouldRespondWith`+          [json|  {"details":"unexpected end of input expecting field name (* or [a..z0..9_])","message":"\"failed to parse columns parameter ()\" (line 1, column 1)"} |]+          { matchStatus  = 400+          , matchHeaders = []+          }++      it "disallows array elements that are not json objects" $+        post "/articles?columns=id,body"+          [json|[+            {"id": 204, "body": "yyy"},+            333,+            "asdf",+            {"id": 205, "body": "zzz"}]|] `shouldRespondWith`+          [json|{+              "code": "22023",+              "details": null,+              "hint": null,+              "message": "argument of json_populate_recordset must be an array of objects"}|]+          { matchStatus  = 400+          , matchHeaders = []+          }++  describe "CSV insert" $ do+    context "disparate csv types" $+      it "succeeds with multipart response" $ do+        pendingWith "Decide on what to do with CSV insert"+        let inserted = [str|integer,double,varchar,boolean,date,money,enum+            |13,3.14159,testing!,false,1900-01-01,$3.99,foo+            |12,0.1,a string,true,1929-10-01,12,bar+            |]+        request methodPost "/menagerie" [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] inserted+           `shouldRespondWith` ResponseMatcher+           { matchStatus  = 201+           , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]+           , matchBody = bodyEquals inserted+           }++    context "requesting full representation" $ do+      it "returns full details of inserted record" $+        request methodPost "/no_pk"+                     [("Content-Type", "text/csv"), ("Accept", "text/csv"),  ("Prefer", "return=representation")]+                     "a,b\nbar,baz"+          `shouldRespondWith` "a,b\nbar,baz"+          { matchStatus  = 201+          , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]+          }++      it "can post nulls" $+        request methodPost "/no_pk"+                     [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]+                     "a,b\nNULL,foo"+          `shouldRespondWith` "a,b\n,foo"+          { matchStatus  = 201+          , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]+          }++      it "only returns the requested column header with its associated data" $+        request methodPost "/projects?select=id"+                     [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]+                     "id,name,client_id\n8,Xenix,1\n9,Windows NT,1"+          `shouldRespondWith` "id\n8\n9"+          { matchStatus  = 201+          , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",+                            "Content-Range" <:> "*/*"]+          }++    context "with wrong number of columns" $+      it "fails for too few" $+        request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz"+        `shouldRespondWith`+        [json|{"message":"All lines must have same number of fields"}|]+        { matchStatus  = 400+        , matchHeaders = [matchContentTypeJson]+        }++    context "with unicode values" $+      it "succeeds and returns usable location header" $ do+        p <- request methodPost "/simple_pk2?select=extra,k"+            [("Prefer", "tx=commit"), ("Prefer", "return=representation")]+            [json| { "k":"圍棋", "extra":"¥" } |]+        pure p `shouldRespondWith`+          [json|[ { "k":"圍棋", "extra":"¥" } ]|]+          { matchStatus = 201 }++        let Just location = lookup hLocation $ simpleHeaders p+        get location+          `shouldRespondWith`+            [json|[ { "k":"圍棋", "extra":"¥" } ]|]++        request methodDelete location+            [("Prefer", "tx=commit")]+            ""+          `shouldRespondWith`+            204++  describe "Row level permission" $+    it "set user_id when inserting rows" $ do+      request methodPost "/authors_only"+          [ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0", ("Prefer", "return=representation") ]+          [json| { "secret": "nyancat" } |]+        `shouldRespondWith`+          [json|[{"owner":"jdoe","secret":"nyancat"}]|]+          { matchStatus  = 201 }++      request methodPost "/authors_only"+          -- jwt token for jroe+          [ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.2e7mx0U4uDcInlbJVOBGlrRufwqWLINDIEDC1vS0nw8", ("Prefer", "return=representation") ]+          [json| { "secret": "lolcat", "owner": "hacker" } |]+        `shouldRespondWith`+          [json|[{"owner":"jroe","secret":"lolcat"}]|]+          { matchStatus  = 201 }++  context "tables with self reference foreign keys" $ do+    it "embeds parent after insert" $+      request methodPost "/web_content?select=id,name,parent_content:p_web_id(name)"+              [("Prefer", "return=representation")]+        [json|{"id":6, "name":"wot", "p_web_id":4}|]+        `shouldRespondWith`+        [json|[{"id":6,"name":"wot","parent_content":{"name":"wut"}}]|]+        { matchStatus  = 201+        , matchHeaders = [ matchContentTypeJson , "Location" <:> "/web_content?id=eq.6" ]+        }++  context "table with limited privileges" $ do+    it "succeeds inserting if correct select is applied" $+      request methodPost "/limited_article_stars?select=article_id,user_id" [("Prefer", "return=representation")]+        [json| {"article_id": 2, "user_id": 1} |] `shouldRespondWith` [json|[{"article_id":2,"user_id":1}]|]+        { matchStatus  = 201+        , matchHeaders = []+        }++    it "fails inserting if more columns are selected" $+      request methodPost "/limited_article_stars?select=article_id,user_id,created_at" [("Prefer", "return=representation")]+        [json| {"article_id": 2, "user_id": 2} |] `shouldRespondWith` (+      if actualPgVersion >= pgVersion112 then+      [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]+         else+      [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]+                                                                      )+        { matchStatus  = 401+        , matchHeaders = []+        }++    it "fails inserting if select is not specified" $+      request methodPost "/limited_article_stars" [("Prefer", "return=representation")]+        [json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` (+      if actualPgVersion >= pgVersion112 then+      [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]+         else+      [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]+                                                                      )+        { matchStatus  = 401+        , matchHeaders = []+        }++    it "can insert in a table with no select and return=minimal" $ do+      request methodPost "/insertonly"+          [("Prefer", "return=minimal")]+          [json| { "v":"some value" } |]+        `shouldRespondWith`+          ""+          { matchStatus = 201 }++  describe "Inserting into VIEWs" $ do+    context "requesting no representation" $+      it "succeeds with 201" $+        post "/compound_pk_view"+            [json|{"k1":1,"k2":"test","extra":2}|]+          `shouldRespondWith`+            ""+            { matchStatus  = 201+            , matchHeaders = [ matchHeaderAbsent hLocation ]+            }++    context "requesting header only representation" $ do+      it "returns a location header" $+        request methodPost "/compound_pk_view" [("Prefer", "return=headers-only")]+            [json|{"k1":1,"k2":"test","extra":2}|]+          `shouldRespondWith`+            ""+            { matchStatus  = 201+            , matchHeaders = [ "Location" <:> "/compound_pk_view?k1=eq.1&k2=eq.test"+                             , "Content-Range" <:> "*/*" ]+            }++      it "should not throw and return location header when a PK is null" $+        request methodPost "/test_null_pk_competitors_sponsors" [("Prefer", "return=headers-only")]+            [json|{"id":1}|]+          `shouldRespondWith`+            ""+            { matchStatus  = 201+            , matchHeaders = [ "Location" <:> "/test_null_pk_competitors_sponsors?id=eq.1&sponsor_id=is.null"+                             , "Content-Range" <:> "*/*" ]+            }
+ test/spec/Feature/JsonOperatorSpec.hs view
@@ -0,0 +1,253 @@+module Feature.JsonOperatorSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import PostgREST.Config.PgVersion (PgVersion, pgVersion112,+                                   pgVersion121)++import Protolude  hiding (get)+import SpecHelper++spec :: PgVersion -> SpecWith ((), Application)+spec actualPgVersion = 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` (+        if actualPgVersion >= pgVersion121 then+        [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for type integer: \"baz\""} |]+        else+        [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]+                            )+        { matchStatus  = 400 , matchHeaders = [] }++    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` (+        if actualPgVersion >= pgVersion112 then+        [json|+          {"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.",+           "details":null,"code":"42883","message":"operator does not exist: text -> integer"} |]+           else+        [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` (+        if actualPgVersion >= pgVersion112 then+        [json|+          {"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.",+           "details":null,"code":"42883","message":"operator does not exist: text -> unknown"} |]+           else+        [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_table?data->foo->>bar=eq.baz" `shouldRespondWith`+        [json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]+        { matchHeaders = [matchContentTypeJson] }+      get "/json_table?data->foo->>bar=eq.fake" `shouldRespondWith`+        [json| [] |]+        { matchHeaders = [matchContentTypeJson] }++    it "can filter by properties inside json column using not" $+      get "/json_table?data->foo->>bar=not.eq.baz" `shouldRespondWith`+        [json| [] |]+        { matchHeaders = [matchContentTypeJson] }++    it "can filter by properties inside json column using ->>" $+      get "/json_table?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_table?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_table?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+      request methodPatch "/json_table?data->>id=eq.3"+          [("Prefer", "return=representation")]+          [json| { "data": { "id":" \"escaped" } } |]+        `shouldRespondWith`+          [json| [{ "data": { "id":" \"escaped" } }] |]++  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] }
+ test/spec/Feature/LegacyGucsSpec.hs view
@@ -0,0 +1,68 @@+module Feature.LegacyGucsSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec          hiding (pendingWith)+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude  hiding (get)+import SpecHelper++spec :: SpecWith ((), Application)+spec =+  describe "remote procedure call with legacy gucs disabled" $ do+    it "custom header is set" $+      request methodPost "/rpc/get_guc_value" [("Custom-Header", "test")]+        [json| { "prefix": "request.headers", "name": "custom-header" } |]+          `shouldRespondWith`+          [json|"test"|]+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson ]+          }++    it "standard header is set" $+      request methodPost "/rpc/get_guc_value" [("Origin", "http://example.com")]+        [json| { "prefix": "request.headers", "name": "origin" } |]+          `shouldRespondWith`+          [json|"http://example.com"|]+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson ]+          }++    it "current role is available as GUC claim" $+      request methodPost "/rpc/get_guc_value" []+        [json| { "prefix": "request.jwt.claims", "name": "role" } |]+          `shouldRespondWith`+          [json|"postgrest_test_anonymous"|]+          { matchStatus  = 200+          , matchHeaders = [ matchContentTypeJson ]+          }++    it "single cookie ends up as claims" $+      request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]+        [json| {"prefix": "request.cookies", "name":"acookie"} |]+          `shouldRespondWith`+          [json|"cookievalue"|]+          { matchStatus = 200+          , matchHeaders = []+          }++    it "multiple cookies ends up as claims" $+      request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]+        [json| {"prefix": "request.cookies", "name":"secondcookie"} |]+          `shouldRespondWith`+          [json|"anothervalue"|]+          { matchStatus = 200+          , matchHeaders = []+          }++    it "gets the Authorization value" $+      request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]+        [json| {"prefix": "request.headers", "name":"authorization"} |]+          `shouldRespondWith`+          [json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]+          { matchStatus = 200+          , matchHeaders = []+          }
+ test/spec/Feature/MultipleSchemaSpec.hs view
@@ -0,0 +1,331 @@+module Feature.MultipleSchemaSpec where++import Control.Lens    ((^?))+import Data.Aeson.Lens+import Data.Aeson.QQ++import Network.HTTP.Types+import Network.Wai        (Application)+import Network.Wai.Test   (SResponse (simpleHeaders), simpleBody)++import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude+import SpecHelper++spec :: SpecWith ((), Application)+spec =+  describe "multiple schemas in single instance" $ do+    context "Reading tables on different schemas" $ do+      it "succeeds in reading table from default schema v1 if no schema is selected via header" $+        request methodGet "/parents" [] "" `shouldRespondWith`+          [json|[+            {"id":1,"name":"parent v1-1"},+            {"id":2,"name":"parent v1-2"}+          ]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]+          }++      it "succeeds in reading table from default schema v1 after explicitly passing it in the header" $+        request methodGet "/parents" [("Accept-Profile", "v1")] "" `shouldRespondWith`+          [json|[+            {"id":1,"name":"parent v1-1"},+            {"id":2,"name":"parent v1-2"}+          ]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]+          }++      it "succeeds in reading table from schema v2" $+        request methodGet "/parents" [("Accept-Profile", "v2")] "" `shouldRespondWith`+          [json|[+            {"id":3,"name":"parent v2-3"},+            {"id":4,"name":"parent v2-4"}+          ]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]+          }++      it "succeeds in reading another_table from schema v2" $+        request methodGet "/another_table" [("Accept-Profile", "v2")] "" `shouldRespondWith`+          [json|[+            {"id":5,"another_value":"value 5"},+            {"id":6,"another_value":"value 6"}+          ]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]+          }++      it "doesn't find another_table in schema v1" $+        request methodGet "/another_table" [("Accept-Profile", "v1")] "" `shouldRespondWith` 404++      it "fails trying to read table from unkown schema" $+        request methodGet "/parents" [("Accept-Profile", "unkown")] "" `shouldRespondWith`+          [json|{"message":"The schema must be one of the following: v1, v2"}|]+          {+            matchStatus = 406+          }++    context "Inserting tables on different schemas" $ do+      it "succeeds inserting on default schema and returning it" $+        request methodPost "/children"+            [("Prefer", "return=representation")]+            [json|{"id": 0, "name": "child v1-1", "parent_id": 1}|]+          `shouldRespondWith`+            [json|[{"id": 0, "name": "child v1-1", "parent_id": 1}]|]+            {+              matchStatus = 201+            , matchHeaders = ["Content-Profile" <:> "v1"]+            }++      it "succeeds inserting on the v1 schema and returning its parent" $+        request methodPost "/children?select=id,parent(*)"+            [("Prefer", "return=representation"), ("Content-Profile", "v1")]+            [json|{"id": 0, "name": "child v1-2", "parent_id": 2}|]+          `shouldRespondWith`+            [json|[{"id": 0, "parent": {"id": 2, "name": "parent v1-2"}}]|]+            {+              matchStatus = 201+            , matchHeaders = ["Content-Profile" <:> "v1"]+            }++      it "succeeds inserting on the v2 schema and returning its parent" $+        request methodPost "/children?select=id,parent(*)"+            [("Prefer", "return=representation"), ("Content-Profile", "v2")]+            [json|{"id": 0, "name": "child v2-3", "parent_id": 3}|]+          `shouldRespondWith`+            [json|[{"id": 0, "parent": {"id": 3, "name": "parent v2-3"}}]|]+            {+              matchStatus = 201+            , matchHeaders = ["Content-Profile" <:> "v2"]+            }++      it "fails when inserting on an unknown schema" $+        request methodPost "/children" [("Content-Profile", "unknown")]+          [json|{"name": "child 4", "parent_id": 4}|]+          `shouldRespondWith`+          [json|{"message":"The schema must be one of the following: v1, v2"}|]+          {+            matchStatus = 406+          }++    context "calling procs on different schemas" $ do+      it "succeeds in calling the default schema proc" $+        request methodGet "/rpc/get_parents_below?id=6" [] ""+          `shouldRespondWith`+          [json|[{"id":1,"name":"parent v1-1"}, {"id":2,"name":"parent v1-2"}]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]+          }++      it "succeeds in calling the v1 schema proc and embedding" $+        request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v1")] ""+          `shouldRespondWith`+          [json| [+            {"id":1,"name":"parent v1-1","children":[{"id":1,"name":"child v1-1"}]},+            {"id":2,"name":"parent v1-2","children":[{"id":2,"name":"child v1-2"}]}] |]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]+          }++      it "succeeds in calling the v2 schema proc and embedding" $+        request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v2")] ""+          `shouldRespondWith`+          [json| [+            {"id":3,"name":"parent v2-3","children":[{"id":1,"name":"child v2-3"}]},+            {"id":4,"name":"parent v2-4","children":[]}] |]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]+          }++      it "succeeds in calling the v2 schema proc with POST by using Content-Profile" $+        request methodPost "/rpc/get_parents_below?select=id,name" [("Content-Profile", "v2")]+          [json|{"id": "6"}|]+          `shouldRespondWith`+          [json| [+            {"id":3,"name":"parent v2-3"},+            {"id":4,"name":"parent v2-4"}]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]+          }++    context "Modifying tables on different schemas" $ do+      it "succeeds in patching on the v1 schema and returning its parent" $+        request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v1"), ("Prefer", "return=representation")]+          [json|{"name": "child v1-1 updated"}|]+          `shouldRespondWith`+          [json|[{"name":"child v1-1 updated", "parent": {"name": "parent v1-1"}}]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]+          }++      it "succeeds in patching on the v2 schema and returning its parent" $+        request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")]+          [json|{"name": "child v2-1 updated"}|]+          `shouldRespondWith`+          [json|[{"name":"child v2-1 updated", "parent": {"name": "parent v2-3"}}]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]+          }++      it "succeeds on deleting on the v2 schema" $ do+        request methodDelete "/children?id=eq.1"+            [("Content-Profile", "v2"), ("Prefer", "return=representation")]+            ""+          `shouldRespondWith`+            [json|[{"id": 1, "name": "child v2-3", "parent_id": 3}]|]+            { matchHeaders = ["Content-Profile" <:> "v2"] }++      it "succeeds on PUT on the v2 schema" $+        request methodPut "/children?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]+          [json| [ { "id": 111, "name": "child v2-111", "parent_id": null } ]|]+          `shouldRespondWith`+          [json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]+          }++    context "OpenAPI output" $ do+      it "succeeds in reading table definition from default schema v1 if no schema is selected via header" $ do+          req <- request methodGet "/" [] ""++          liftIO $ do+            simpleHeaders req `shouldSatisfy` matchHeader "Content-Profile" "v1"++            let def = simpleBody req ^? key "definitions" . key "parents"++            def `shouldBe` Just+                [aesonQQ|+                  {+                    "type" : "object",+                    "properties" : {+                      "id" : {+                        "description" : "Note:\nThis is a Primary Key.<pk/>",+                        "format" : "integer",+                        "type" : "integer"+                      },+                      "name" : {+                        "format" : "text",+                        "type" : "string"+                      }+                    },+                    "required" : [+                      "id"+                    ]+                  }+                |]++      it "succeeds in reading table definition from default schema v1 after explicitly passing it in the header" $ do+          r <- request methodGet "/" [("Accept-Profile", "v1")] ""++          liftIO $ do+            simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v1"++            let def = simpleBody r ^? key "definitions" . key "parents"++            def `shouldBe` Just+                [aesonQQ|+                  {+                    "type" : "object",+                    "properties" : {+                      "id" : {+                        "description" : "Note:\nThis is a Primary Key.<pk/>",+                        "format" : "integer",+                        "type" : "integer"+                      },+                      "name" : {+                        "format" : "text",+                        "type" : "string"+                      }+                    },+                    "required" : [+                      "id"+                    ]+                  }+                |]++      it "succeeds in reading table definition from schema v2" $ do+          r <- request methodGet "/" [("Accept-Profile", "v2")] ""++          liftIO $ do+            simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v2"++            let def = simpleBody r ^? key "definitions" . key "parents"++            def `shouldBe` Just+                [aesonQQ|+                  {+                    "type" : "object",+                    "properties" : {+                      "id" : {+                        "description" : "Note:\nThis is a Primary Key.<pk/>",+                        "format" : "integer",+                        "type" : "integer"+                      },+                      "name" : {+                        "format" : "text",+                        "type" : "string"+                      }+                    },+                    "required" : [+                      "id"+                    ]+                  }+                |]++      it "succeeds in reading another_table definition from schema v2" $ do+          r <- request methodGet "/" [("Accept-Profile", "v2")] ""++          liftIO $ do+            simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v2"++            let def = simpleBody r ^? key "definitions" . key "another_table"++            def `shouldBe` Just+                [aesonQQ|+                  {+                    "type" : "object",+                    "properties" : {+                      "id" : {+                        "description" : "Note:\nThis is a Primary Key.<pk/>",+                        "format" : "integer",+                        "type" : "integer"+                      },+                      "another_value" : {+                        "format" : "text",+                        "type" : "string"+                      }+                    },+                    "required" : [+                      "id"+                    ]+                  }+                |]++      it "doesn't find another_table definition in schema v1" $ do+        r <- request methodGet "/" [("Accept-Profile", "v1")] ""++        liftIO $ do+          let def = simpleBody r ^? key "definitions" . key "another_table"+          def `shouldBe` Nothing++      it "fails trying to read definitions from unkown schema" $+        request methodGet "/" [("Accept-Profile", "unkown")] "" `shouldRespondWith`+          [json|{"message":"The schema must be one of the following: v1, v2"}|]+          {+            matchStatus = 406+          }
+ test/spec/Feature/NoJwtSpec.hs view
@@ -0,0 +1,31 @@+module Feature.NoJwtSpec where++-- {{{ Imports++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude+import SpecHelper+-- }}}++spec :: SpecWith ((), Application)+spec = describe "server started without JWT secret" $ do++  -- this test will stop working 9999999999s after the UNIX EPOCH+  it "responds with error on attempted auth" $ do+    let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"+    request methodGet "/authors_only" [auth] ""+      `shouldRespondWith`+      [json|{"message":"Server lacks JWT secret"}|]+      { matchStatus  = 500+      , matchHeaders = [ matchContentTypeJson ]+      }++  it "behaves normally when user does not attempt auth" $+    request methodGet "/items" [] ""+      `shouldRespondWith` 200
+ test/spec/Feature/NonexistentSchemaSpec.hs view
@@ -0,0 +1,17 @@+module Feature.NonexistentSchemaSpec where++import Network.Wai (Application)++import Test.Hspec+import Test.Hspec.Wai++import Protolude hiding (get)++spec :: SpecWith ((), Application)+spec =+  describe "Non existent api schema" $ do+    it "succeeds when requesting root path" $+      get "/" `shouldRespondWith` 200++    it "gives 404 when requesting a nonexistent table in this nonexistent schema" $+      get "/nonexistent_table" `shouldRespondWith` 404
+ test/spec/Feature/OpenApiSpec.hs view
@@ -0,0 +1,615 @@+module Feature.OpenApiSpec where++import Control.Lens     ((^?))+import Data.Aeson.Types (Value (..))+import Network.Wai      (Application)+import Network.Wai.Test (SResponse (..))++import Data.Aeson.Lens+import Data.Aeson.QQ+import Network.HTTP.Types+import Test.Hspec         hiding (pendingWith)+import Test.Hspec.Wai++import PostgREST.Config.PgVersion (PgVersion, pgVersion100,+                                   pgVersion110)++import PostgREST.Version (docsVersion)+import Protolude         hiding (get)+import SpecHelper++spec :: PgVersion -> SpecWith ((), Application)+spec actualPgVersion = describe "OpenAPI" $ do+  it "root path returns a valid openapi spec" $ do+    validateOpenApiResponse [("Accept", "application/openapi+json")]+    request methodHead "/" (acceptHdrs "application/openapi+json") ""+      `shouldRespondWith` "" { matchStatus  = 200 }++  it "should respond to openapi request on none root path with 415" $+    request methodGet "/items"+            (acceptHdrs "application/openapi+json") ""+      `shouldRespondWith` 415++  it "includes postgrest.org current version api docs" $ do+    r <- simpleBody <$> get "/"++    let docsUrl = r ^? key "externalDocs" . key "url"++    liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))++  describe "table" $ do++    it "includes paths to tables" $ do+      r <- simpleBody <$> get "/"++      let method s = key "paths" . key "/child_entities" . key s+          childGetSummary = r ^? method "get" . key "summary"+          childGetDescription = r ^? method "get" . key "description"+          getParameters = r ^? method "get" . key "parameters"+          postParameters = r ^? method "post" . key "parameters"+          postResponse = r ^? method "post" . key "responses" . key "201" . key "description"+          patchResponse = r ^? method "patch" . key "responses" . key "204" . key "description"+          deleteResponse = r ^? method "delete" . key "responses" . key "204" . key "description"++      let grandChildGet s = key "paths" . key "/grandchild_entities" . key "get" . key s+          grandChildGetSummary = r ^? grandChildGet "summary"+          grandChildGetDescription = r ^? grandChildGet "description"++      liftIO $ do++        childGetSummary `shouldBe` Just "child_entities comment"++        childGetDescription `shouldBe` Nothing++        grandChildGetSummary `shouldBe` Just "grandchild_entities summary"++        grandChildGetDescription `shouldBe` Just "grandchild_entities description\nthat spans\nmultiple lines"++        getParameters `shouldBe` Just+          [aesonQQ|+            [+              { "$ref": "#/parameters/rowFilter.child_entities.id" },+              { "$ref": "#/parameters/rowFilter.child_entities.name" },+              { "$ref": "#/parameters/rowFilter.child_entities.parent_id" },+              { "$ref": "#/parameters/select" },+              { "$ref": "#/parameters/order" },+              { "$ref": "#/parameters/range" },+              { "$ref": "#/parameters/rangeUnit" },+              { "$ref": "#/parameters/offset" },+              { "$ref": "#/parameters/limit" },+              { "$ref": "#/parameters/preferCount" }+            ]+          |]++        postParameters `shouldBe` Just+          [aesonQQ|+            [+              { "$ref": "#/parameters/body.child_entities" },+              { "$ref": "#/parameters/select" },+              { "$ref": "#/parameters/preferReturn" }+            ]+          |]++        postResponse `shouldBe` Just "Created"++        patchResponse `shouldBe` Just "No Content"++        deleteResponse `shouldBe` Just "No Content"++    it "includes an array type for GET responses" $ do+      r <- simpleBody <$> get "/"++      let childGetSchema = r ^? key "paths"+            . key "/child_entities"+            . key "get"+            . key "responses"+            . key "200"+            . key "schema"++      liftIO $+        childGetSchema `shouldBe` Just+          [aesonQQ|+            {+              "items": {+                "$ref": "#/definitions/child_entities"+              },+              "type": "array"+            }+          |]++    it "includes definitions to tables" $ do+      r <- simpleBody <$> get "/"++      let def = r ^? key "definitions" . key "child_entities"++      liftIO $++        def `shouldBe` Just+          [aesonQQ|+            {+              "type": "object",+              "description": "child_entities comment",+              "properties": {+                "id": {+                  "description": "child_entities id comment\n\nNote:\nThis is a Primary Key.<pk/>",+                  "format": "integer",+                  "type": "integer"+                },+                "name": {+                  "description": "child_entities name comment. Can be longer than sixty-three characters long",+                  "format": "text",+                  "type": "string"+                },+                "parent_id": {+                  "description": "Note:\nThis is a Foreign Key to `entities.id`.<fk table='entities' column='id'/>",+                  "format": "integer",+                  "type": "integer"+                }+              },+              "required": [+                "id"+              ]+            }+          |]++    it "doesn't include privileged table for anonymous" $ do+      r <- simpleBody <$> get "/"+      let tablePath = r ^? key "paths" . key "/authors_only"++      liftIO $ tablePath `shouldBe` Nothing++    it "includes table if user has permission" $ do+      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"+      r <- simpleBody <$> request methodGet "/" [auth] ""+      let tableTag = r ^? key "paths" . key "/authors_only"+                    . key "post"  . key "tags"+                    . nth 0+      liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|]++  describe "Foreign table" $++    it "includes foreign table properties" $ do+      r <- simpleBody <$> get "/"++      let method s = key "paths" . key "/projects_dump" . key s+          getSummary = r ^? method "get" . key "summary"+          getDescription = r ^? method "get" . key "description"+          getParameters = r ^? method "get" . key "parameters"++      liftIO $ do++        getSummary `shouldBe` Just "A temporary projects dump"++        getDescription `shouldBe` Just "Just a test for foreign tables"++        getParameters `shouldBe` Just+          [aesonQQ|+            [+              { "$ref": "#/parameters/rowFilter.projects_dump.id" },+              { "$ref": "#/parameters/rowFilter.projects_dump.name" },+              { "$ref": "#/parameters/rowFilter.projects_dump.client_id" },+              { "$ref": "#/parameters/select" },+              { "$ref": "#/parameters/order" },+              { "$ref": "#/parameters/range" },+              { "$ref": "#/parameters/rangeUnit" },+              { "$ref": "#/parameters/offset" },+              { "$ref": "#/parameters/limit" },+              { "$ref": "#/parameters/preferCount" }+            ]+          |]++  when (actualPgVersion >= pgVersion100) $ do+    describe "Partitioned table" $++      it "includes partitioned table properties" $ do+        r <- simpleBody <$> get "/"++        let method s = key "paths" . key "/car_models" . key s+            getSummary = r ^? method "get" . key "summary"+            getDescription = r ^? method "get" . key "description"+            getParameterName = r ^? method "get" . key "parameters" . nth 0 . key "$ref"+            getParameterYear = r ^? method "get" . key "parameters" . nth 1 . key "$ref"+            getParameterRef = r ^? method "get" . key "parameters" . nth 2 . key "$ref"++        liftIO $ do++          getSummary `shouldBe` Just "A partitioned table"++          getDescription `shouldBe` Just "A test for partitioned tables"++          getParameterName `shouldBe` Just "#/parameters/rowFilter.car_models.name"++          getParameterYear `shouldBe` Just "#/parameters/rowFilter.car_models.year"++          when (actualPgVersion >= pgVersion110) $+            getParameterRef `shouldBe` Just "#/parameters/rowFilter.car_models.car_brand_name"++  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 "VIEW that has a source FK based on a UNIQUE key" $++    it "includes fk description" $ do+      r <- simpleBody <$> get "/"++      let referralLink = r ^? key "definitions" . key "referrals" . key "properties" . key "link"++      liftIO $+        referralLink `shouldBe` Just+          [aesonQQ|+            {+              "format": "integer",+              "type": "integer",+              "description": "Note:\nThis is a Foreign Key to `pages.link`.<fk table='pages' column='link'/>"+            }+          |]++  describe "PostgreSQL to Swagger Type Mapping" $ do++    it "character varying to string" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_character_varying"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "character varying",+              "type": "string"+            }+          |]+    it "character(1) to string" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_character"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "maxLength": 1,+              "format": "character",+              "type": "string"+            }+          |]++    it "text to string" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_text"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "text",+              "type": "string"+            }+          |]++    it "boolean to boolean" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_boolean"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "boolean",+              "type": "boolean"+            }+          |]++    it "smallint to integer" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_smallint"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "smallint",+              "type": "integer"+            }+          |]++    it "integer to integer" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_integer"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "integer",+              "type": "integer"+            }+          |]++    it "bigint to integer" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_bigint"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "bigint",+              "type": "integer"+            }+          |]++    it "numeric to number" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_numeric"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "numeric",+              "type": "number"+            }+          |]++    it "real to number" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_real"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "real",+              "type": "number"+            }+          |]++    it "double_precision to number" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_double_precision"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "double precision",+              "type": "number"+            }+          |]++    it "json to any" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_json"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "json"+            }+          |]++    it "jsonb to any" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_jsonb"++      liftIO $++        types `shouldBe` Just+          [aesonQQ|+            {+              "format": "jsonb"+            }+          |]++  describe "Detects default values" $ do++    it "text" $ do+      r <- simpleBody <$> get "/"++      let defaultValue = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "text" . key "default"++      liftIO $++        defaultValue `shouldBe` Just "default"++    it "boolean" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "boolean" . key "default"++      liftIO $++        types `shouldBe` Just (Bool False)++    it "integer" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "integer" . key "default"++      liftIO $++        types `shouldBe` Just (Number 42)++    it "numeric" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "numeric" . key "default"++      liftIO $++        types `shouldBe` Just (Number 42.2)++    it "date" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "date" . key "default"++      liftIO $++        types `shouldBe` Just "1900-01-01"++    it "time" $ do+      r <- simpleBody <$> get "/"++      let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "time" . key "default"++      liftIO $++        types `shouldBe` Just "13:00:00"++  describe "RPC" $ do++    it "includes function summary/description and body schema for arguments" $ do+      r <- simpleBody <$> get "/"++      let method s = key "paths" . key "/rpc/varied_arguments" . key s+          args = r ^? method "post" . key "parameters" . nth 0 . key "schema"+          summary = r ^? method "post" . key "summary"+          description = r ^? method "post" . key "description"++      liftIO $ do++        summary `shouldBe` Just "An RPC function"++        description `shouldBe` Just "Just a test for RPC function arguments"++        args `shouldBe` Just+          [aesonQQ|+            {+              "required": [+                "double",+                "varchar",+                "boolean",+                "date",+                "money",+                "enum",+                "arr"+              ],+              "properties": {+                "double": {+                  "format": "double precision",+                  "type": "number"+                },+                "varchar": {+                  "format": "character varying",+                  "type": "string"+                },+                "boolean": {+                  "format": "boolean",+                  "type": "boolean"+                },+                "date": {+                  "format": "date",+                  "type": "string"+                },+                "money": {+                  "format": "money",+                  "type": "string"+                },+                "enum": {+                  "format": "enum_menagerie_type",+                  "type": "string"+                },+                "arr": {+                  "format": "text[]",+                  "type": "string"+                },+                "integer": {+                  "format": "integer",+                  "type": "integer"+                },+                "json": {+                  "format": "json"+                },+                "jsonb": {+                  "format": "jsonb"+                }+              },+              "type": "object",+              "description": "An RPC function\n\nJust a test for RPC function arguments"+            }+          |]++    it "doesn't include privileged function for anonymous" $ do+      r <- simpleBody <$> get "/"+      let funcPath = r ^? key "paths" . key "/rpc/privileged_hello"++      liftIO $ funcPath `shouldBe` Nothing++    it "includes function if user has permission" $ do+      let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"+      r <- simpleBody <$> request methodGet "/" [auth] ""+      let funcTag = r ^? key "paths" . key "/rpc/privileged_hello"+                    . key "post"  . key "tags"+                    . nth 0++      liftIO $ funcTag `shouldBe` Just [aesonQQ|"(rpc) privileged_hello"|]++    it "doesn't include OUT params of function as required parameters" $ do+      r <- simpleBody <$> get "/"+      let params = r ^? key "paths" . key "/rpc/many_out_params"+                      . key "post" . key "parameters" .  nth 0+                      . key "schema". key "required"++      liftIO $ params `shouldBe` Nothing++    it "includes INOUT params(with no DEFAULT) of function as required parameters" $ do+      r <- simpleBody <$> get "/"+      let params = r ^? key "paths" . key "/rpc/many_inout_params"+                      . key "post" . key "parameters" .  nth 0+                      . key "schema". key "required"++      liftIO $ params `shouldBe` Just [aesonQQ|["num", "str"]|]+
+ test/spec/Feature/OptionsSpec.hs view
@@ -0,0 +1,87 @@+module Feature.OptionsSpec where++import Network.Wai      (Application)+import Network.Wai.Test (SResponse (..))++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai++import PostgREST.Config.PgVersion (PgVersion, pgVersion100,+                                   pgVersion110)++import Protolude+import SpecHelper++spec :: PgVersion -> SpecWith ((), Application)+spec actualPgVersion = describe "Allow header" $ do+  context "a table" $ do+    it "includes read/write verbs for writeable table" $ do+      r <- request methodOptions "/items" [] ""+      liftIO $+        simpleHeaders r `shouldSatisfy`+          matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"++  when (actualPgVersion >= pgVersion100) $+    context "a partitioned table" $ do+      it "includes read/write verbs for writeable partitioned tables" $ do+        r <- request methodOptions "/car_models" [] ""+        liftIO $+          simpleHeaders r `shouldSatisfy`+            matchHeader "Allow" (+              if actualPgVersion >= pgVersion110 then+                "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"+              else+                "OPTIONS,GET,HEAD,POST,PATCH,DELETE"+            )++  context "a view" $ do+    context "auto updatable" $ do+      it "includes read/write verbs for auto updatable views with pk" $ do+        r <- request methodOptions "/projects_auto_updatable_view_with_pk" [] ""+        liftIO $+          simpleHeaders r `shouldSatisfy`+            matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"++      it "includes read/write verbs for auto updatable views without pk" $ do+        r <- request methodOptions "/projects_auto_updatable_view_without_pk" [] ""+        liftIO $+          simpleHeaders r `shouldSatisfy`+            matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PATCH,DELETE"++    context "non auto updatable" $ do+      it "includes read verbs for non auto updatable views" $ do+        r <- request methodOptions "/projects_view_without_triggers" [] ""+        liftIO $+          simpleHeaders r `shouldSatisfy`+            matchHeader "Allow" "OPTIONS,GET,HEAD"++      it "includes read/write verbs for insertable, updatable and deletable views with pk" $ do+        r <- request methodOptions "/projects_view_with_all_triggers_with_pk" [] ""+        liftIO $+          simpleHeaders r `shouldSatisfy`+            matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"++      it "includes read/write verbs for insertable, updatable and deletable views without pk" $ do+        r <- request methodOptions "/projects_view_with_all_triggers_without_pk" [] ""+        liftIO $+          simpleHeaders r `shouldSatisfy`+            matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PATCH,DELETE"++      it "includes read and insert verbs for insertable views" $ do+        r <- request methodOptions "/projects_view_with_insert_trigger" [] ""+        liftIO $+          simpleHeaders r `shouldSatisfy`+            matchHeader "Allow" "OPTIONS,GET,HEAD,POST"++      it "includes read and update verbs for updatable views" $ do+        r <- request methodOptions "/projects_view_with_update_trigger" [] ""+        liftIO $+          simpleHeaders r `shouldSatisfy`+            matchHeader "Allow" "OPTIONS,GET,HEAD,PATCH"++      it "includes read and delete verbs for deletable views" $ do+        r <- request methodOptions "/projects_view_with_delete_trigger" [] ""+        liftIO $+          simpleHeaders r `shouldSatisfy`+            matchHeader "Allow" "OPTIONS,GET,HEAD,DELETE"
+ test/spec/Feature/ProxySpec.hs view
@@ -0,0 +1,13 @@+module Feature.ProxySpec where++import Network.Wai (Application)+import Test.Hspec  hiding (pendingWith)++import Protolude+import SpecHelper++spec :: SpecWith ((), Application)+spec =+  describe "GET / with proxy" $+    it "returns a valid openapi spec with proxy" $+      validateOpenApiResponse [("Accept", "application/openapi+json")]
+ test/spec/Feature/QueryLimitedSpec.hs view
@@ -0,0 +1,69 @@+module Feature.QueryLimitedSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude  hiding (get)+import SpecHelper++spec :: SpecWith ((), Application)+spec =+  describe "Requesting many items with server limits(max-rows) enabled" $ do+    it "restricts results" $+      get "/items?order=id"+        `shouldRespondWith`+          [json| [{"id":1},{"id":2}] |]+          { matchHeaders = ["Content-Range" <:> "0-1/*"] }++    it "respects additional client limiting" $ do+      request methodGet  "/items"+          (rangeHdrs $ ByteRangeFromTo 0 0)+          ""+        `shouldRespondWith`+          [json| [{"id":1}] |]+          { matchHeaders = ["Content-Range" <:> "0-0/*"] }++    it "works on all levels" $+      get "/users?select=id,tasks(id)&order=id.asc&tasks.order=id.asc"+        `shouldRespondWith`+          [json|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]+          { matchHeaders = ["Content-Range" <:> "0-1/*"] }++    it "succeeds in getting parent embeds despite the limit, see #647" $+      get "/tasks?select=id,project:projects(id)&id=gt.5"+        `shouldRespondWith`+          [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]+          { matchHeaders = ["Content-Range" <:> "0-1/*"] }++    it "can offset the parent embed, being consistent with the other embed types" $+      get "/tasks?select=id,project:projects(id)&id=gt.5&project.offset=1"+        `shouldRespondWith`+          [json|[{"id":6,"project":null}, {"id":7,"project":null}]|]+          { matchHeaders = ["Content-Range" <:> "0-1/*"] }++    context "count=estimated" $ do+      it "uses the query planner guess when query rows > maxRows" $+        request methodHead "/getallprojects_view" [("Prefer", "count=estimated")] ""+          `shouldRespondWith`+            ""+            { matchStatus  = 206+            , matchHeaders = ["Content-Range" <:> "0-1/2019"]+            }++      it "gives exact count when query rows <= maxRows" $+        request methodHead "/getallprojects_view?id=lt.3" [("Prefer", "count=estimated")] ""+          `shouldRespondWith`+            ""+            { matchHeaders = ["Content-Range" <:> "0-1/2"] }++      it "only uses the query planner guess if it's indeed greater than the exact count" $+        request methodHead "/get_projects_above_view" [("Prefer", "count=estimated")] ""+          `shouldRespondWith`+            ""+            { matchStatus  = 206+            , matchHeaders = ["Content-Range" <:> "0-1/3"]+            }
+ test/spec/Feature/QuerySpec.hs view
@@ -0,0 +1,1125 @@+module Feature.QuerySpec where++import Network.Wai      (Application)+import Network.Wai.Test (SResponse (simpleHeaders))++import Network.HTTP.Types+import Test.Hspec          hiding (pendingWith)+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import PostgREST.Config.PgVersion (PgVersion, pgVersion110,+                                   pgVersion112, pgVersion121)+import Protolude                  hiding (get)+import SpecHelper++spec :: PgVersion -> SpecWith ((), Application)+spec actualPgVersion = do++  describe "Querying a table with a column called count" $+    it "should not confuse count column with pg_catalog.count aggregate" $+      get "/has_count_column" `shouldRespondWith` 200++  describe "Querying a table with a column called t" $+    it "should not conflict with internal postgrest table alias" $+      get "/clashing_column?select=t" `shouldRespondWith` 200++  describe "Querying a nonexistent table" $+    it "causes a 404" $+      get "/faketable" `shouldRespondWith` 404++  describe "Filtering response" $ do+    it "matches with equality" $+      get "/items?id=eq.5"+        `shouldRespondWith` [json| [{"id":5}] |]+        { matchHeaders = ["Content-Range" <:> "0-0/*"] }++    it "matches with equality using not operator" $+      get "/items?id=not.eq.5&order=id"+        `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]+        { matchHeaders = ["Content-Range" <:> "0-13/*"] }++    it "matches with more than one condition using not operator" $+      get "/simple_pk?k=like.*yx&extra=not.eq.u" `shouldRespondWith` "[]"++    it "matches with inequality using not operator" $ do+      get "/items?id=not.lt.14&order=id.asc"+        `shouldRespondWith` [json| [{"id":14},{"id":15}] |]+        { matchHeaders = ["Content-Range" <:> "0-1/*"] }+      get "/items?id=not.gt.2&order=id.asc"+        `shouldRespondWith` [json| [{"id":1},{"id":2}] |]+        { matchHeaders = ["Content-Range" <:> "0-1/*"] }++    it "matches items IN" $+      get "/items?id=in.(1,3,5)"+        `shouldRespondWith` [json| [{"id":1},{"id":3},{"id":5}] |]+        { matchHeaders = ["Content-Range" <:> "0-2/*"] }++    it "matches items NOT IN using not operator" $+      get "/items?id=not.in.(2,4,6,7,8,9,10,11,12,13,14,15)"+        `shouldRespondWith` [json| [{"id":1},{"id":3},{"id":5}] |]+        { matchHeaders = ["Content-Range" <:> "0-2/*"] }++    it "matches nulls using not operator" $+      get "/no_pk?a=not.is.null" `shouldRespondWith`+        [json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]+        { matchHeaders = [matchContentTypeJson] }++    it "matches nulls in varchar and numeric fields alike" $ do+      get "/no_pk?a=is.null" `shouldRespondWith`+        [json| [{"a": null, "b": null}] |]+        { matchHeaders = [matchContentTypeJson] }++      get "/nullable_integer?a=is.null" `shouldRespondWith` [json|[{"a":null}]|]++    it "matches with trilean values" $ do+      get "/chores?done=is.true" `shouldRespondWith`+        [json| [{"id": 1, "name": "take out the garbage", "done": true }] |]+        { matchHeaders = [matchContentTypeJson] }++      get "/chores?done=is.false" `shouldRespondWith`+        [json| [{"id": 2, "name": "do the laundry", "done": false }] |]+        { matchHeaders = [matchContentTypeJson] }++      get "/chores?done=is.unknown" `shouldRespondWith`+        [json| [{"id": 3, "name": "wash the dishes", "done": null }] |]+        { matchHeaders = [matchContentTypeJson] }++    it "matches with trilean values in upper or mixed case" $ do+      get "/chores?done=is.NULL" `shouldRespondWith`+        [json| [{"id": 3, "name": "wash the dishes", "done": null }] |]+        { matchHeaders = [matchContentTypeJson] }++      get "/chores?done=is.TRUE" `shouldRespondWith`+        [json| [{"id": 1, "name": "take out the garbage", "done": true }] |]+        { matchHeaders = [matchContentTypeJson] }++      get "/chores?done=is.FAlSe" `shouldRespondWith`+        [json| [{"id": 2, "name": "do the laundry", "done": false }] |]+        { matchHeaders = [matchContentTypeJson] }++      get "/chores?done=is.UnKnOwN" `shouldRespondWith`+        [json| [{"id": 3, "name": "wash the dishes", "done": null }] |]+        { matchHeaders = [matchContentTypeJson] }++    it "fails if 'is' used and there's no null or trilean value" $ do+      get "/chores?done=is.nil" `shouldRespondWith` 400+      get "/chores?done=is.ok"  `shouldRespondWith` 400++    it "matches with like" $ do+      get "/simple_pk?k=like.*yx" `shouldRespondWith`+        [json|[{"k":"xyyx","extra":"u"}]|]+      get "/simple_pk?k=like.xy*" `shouldRespondWith`+        [json|[{"k":"xyyx","extra":"u"}]|]+      get "/simple_pk?k=like.*YY*" `shouldRespondWith`+        [json|[{"k":"xYYx","extra":"v"}]|]++    it "matches with like using not operator" $+      get "/simple_pk?k=not.like.*yx" `shouldRespondWith`+        [json|[{"k":"xYYx","extra":"v"}]|]++    it "matches with ilike" $ do+      get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`+        [json|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|]+        { matchHeaders = [matchContentTypeJson] }+      get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`+        [json|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "matches with ilike using not operator" $+      get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]"++    describe "Full text search operator" $ do+      it "finds matches with to_tsquery" $+        get "/tsearch?text_search_vector=fts.impossible" `shouldRespondWith`+          [json| [{"text_search_vector": "'fun':5 'imposs':9 'kind':3" }] |]+          { matchHeaders = [matchContentTypeJson] }++      it "can use lexeme boolean operators(&=%26, |=%7C, !) in to_tsquery" $ do+        get "/tsearch?text_search_vector=fts.fun%26possible" `shouldRespondWith`+          [json| [ {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/tsearch?text_search_vector=fts.impossible%7Cpossible" `shouldRespondWith`+          [json| [+          {"text_search_vector": "'fun':5 'imposs':9 'kind':3"},+          {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/tsearch?text_search_vector=fts.fun%26!possible" `shouldRespondWith`+          [json| [ {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}] |]+          { matchHeaders = [matchContentTypeJson] }++      it "finds matches with plainto_tsquery" $+        get "/tsearch?text_search_vector=plfts.The%20Fat%20Rats" `shouldRespondWith`+          [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]+          { matchHeaders = [matchContentTypeJson] }++      when (actualPgVersion >= pgVersion112) $ do+        it "finds matches with websearch_to_tsquery" $+            get "/tsearch?text_search_vector=wfts.The%20Fat%20Rats" `shouldRespondWith`+                [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]+                { matchHeaders = [matchContentTypeJson] }++        it "can use boolean operators(and, or, -) in websearch_to_tsquery" $ do+          get "/tsearch?text_search_vector=wfts.fun%20and%20possible"+            `shouldRespondWith`+              [json| [ {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] |]+              { matchHeaders = [matchContentTypeJson] }+          get "/tsearch?text_search_vector=wfts.impossible%20or%20possible"+            `shouldRespondWith`+              [json| [+                {"text_search_vector": "'fun':5 'imposs':9 'kind':3"},+                {"text_search_vector": "'also':2 'fun':3 'possibl':8"}]+                  |]+              { matchHeaders = [matchContentTypeJson] }+          get "/tsearch?text_search_vector=wfts.fun%20and%20-possible"+            `shouldRespondWith`+              [json| [ {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}] |]+              { matchHeaders = [matchContentTypeJson] }++      it "finds matches with different dictionaries" $ do+        get "/tsearch?text_search_vector=fts(french).amusant" `shouldRespondWith`+          [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/tsearch?text_search_vector=plfts(french).amusant%20impossible" `shouldRespondWith`+          [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |]+          { matchHeaders = [matchContentTypeJson] }++        when (actualPgVersion >= pgVersion112) $+            get "/tsearch?text_search_vector=wfts(french).amusant%20impossible"+                `shouldRespondWith`+                  [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |]+                  { matchHeaders = [matchContentTypeJson] }++      it "can be negated with not operator" $ do+        get "/tsearch?text_search_vector=not.fts.impossible%7Cfat%7Cfun" `shouldRespondWith`+          [json| [+            {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},+            {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/tsearch?text_search_vector=not.fts(english).impossible%7Cfat%7Cfun" `shouldRespondWith`+          [json| [+            {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},+            {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/tsearch?text_search_vector=not.plfts.The%20Fat%20Rats" `shouldRespondWith`+          [json| [+            {"text_search_vector": "'fun':5 'imposs':9 'kind':3"},+            {"text_search_vector": "'also':2 'fun':3 'possibl':8"},+            {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},+            {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]+          { matchHeaders = [matchContentTypeJson] }+        when (actualPgVersion >= pgVersion112) $+            get "/tsearch?text_search_vector=not.wfts(english).impossible%20or%20fat%20or%20fun"+                `shouldRespondWith`+                  [json| [+                    {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},+                    {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]+                  { matchHeaders = [matchContentTypeJson] }++      context "Use of the phraseto_tsquery function" $ do+        it "finds matches" $+          get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith`+            [json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]+            { matchHeaders = [matchContentTypeJson] }++        it "finds matches with different dictionaries" $+          get "/tsearch?text_search_vector=phfts(german).Art%20Spass" `shouldRespondWith`+            [json| [{"text_search_vector": "'art':4 'spass':5 'unmog':7" }] |]+            { matchHeaders = [matchContentTypeJson] }++        it "can be negated with not operator" $+          get "/tsearch?text_search_vector=not.phfts(english).The%20Fat%20Cats" `shouldRespondWith`+            [json| [+              {"text_search_vector": "'fun':5 'imposs':9 'kind':3"},+              {"text_search_vector": "'also':2 'fun':3 'possibl':8"},+              {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},+              {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]+            { matchHeaders = [matchContentTypeJson] }++        it "can be used with or query param" $+          get "/tsearch?or=(text_search_vector.phfts(german).Art%20Spass, text_search_vector.phfts(french).amusant, text_search_vector.fts(english).impossible)" `shouldRespondWith`+            [json|[+              {"text_search_vector": "'fun':5 'imposs':9 'kind':3" },+              {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },+              {"text_search_vector": "'art':4 'spass':5 'unmog':7"}+            ]|] { matchHeaders = [matchContentTypeJson] }++    it "matches with computed column" $+      get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith`+        [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]+        { matchHeaders = [matchContentTypeJson] }++    it "order by computed column" $+      get "/items?order=anti_id.desc" `shouldRespondWith`+        [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]+        { matchHeaders = [matchContentTypeJson] }++    it "cannot access a computed column that is outside of the config schema" $+      get "/items?always_false=is.false" `shouldRespondWith` 400++    it "matches filtering nested items 2" $+      get "/clients?select=id,projects(id,tasks2(id,name))&projects.tasks.name=like.Design*" `shouldRespondWith`+        [json| {+          "hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",+          "message":"Could not find a relationship between projects and tasks2 in the schema cache"}|]+        { matchStatus  = 400+        , matchHeaders = [matchContentTypeJson]+        }++    it "matches filtering nested items" $+      get "/clients?select=id,projects(id,tasks(id,name))&projects.tasks.name=like.Design*" `shouldRespondWith`+        [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1,"name":"Design w7"}]},{"id":2,"tasks":[{"id":3,"name":"Design w10"}]}]},{"id":2,"projects":[{"id":3,"tasks":[{"id":5,"name":"Design IOS"}]},{"id":4,"tasks":[{"id":7,"name":"Design OSX"}]}]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "matches with cs operator" $+      get "/complex_items?select=id&arr_data=cs.{2}" `shouldRespondWith`+        [json|[{"id":2},{"id":3}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "matches with cd operator" $+      get "/complex_items?select=id&arr_data=cd.{1,2,4}" `shouldRespondWith`+        [json|[{"id":1},{"id":2}]|]+        { matchHeaders = [matchContentTypeJson] }++  describe "Shaping response with select parameter" $ do+    it "selectStar works in absense of parameter" $+      get "/complex_items?id=eq.3" `shouldRespondWith`+        [json|[{"id":3,"name":"Three","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}]|]++    it "dash `-` in column names is accepted" $+      get "/complex_items?id=eq.3&select=id,field-with_sep" `shouldRespondWith`+        [json|[{"id":3,"field-with_sep":1}]|]++    it "one simple column" $+      get "/complex_items?select=id" `shouldRespondWith`+        [json| [{"id":1},{"id":2},{"id":3}] |]+        { matchHeaders = [matchContentTypeJson] }++    it "rename simple column" $+      get "/complex_items?id=eq.1&select=myId:id" `shouldRespondWith`+        [json| [{"myId":1}] |]+        { matchHeaders = [matchContentTypeJson] }++    it "one simple column with casting (text)" $+      get "/complex_items?select=id::text" `shouldRespondWith`+        [json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]+        { matchHeaders = [matchContentTypeJson] }++    it "rename simple column with casting" $+      get "/complex_items?id=eq.1&select=myId:id::text" `shouldRespondWith`+        [json| [{"myId":"1"}] |]+        { matchHeaders = [matchContentTypeJson] }++    it "json column" $+      get "/complex_items?id=eq.1&select=settings" `shouldRespondWith`+        [json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]+        { matchHeaders = [matchContentTypeJson] }++    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"} |]+        { matchStatus  = 400+        , matchHeaders = []+        }++    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 and renaming primary key" $+      get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith`+        [json|[+          {"name":"Windows 7","client":{"name": "Microsoft", "clientId": 1}},+          {"name":"Windows 10","client":{"name": "Microsoft", "clientId": 1}},+          {"name":"IOS","client":{"name": "Apple", "clientId": 2}},+          {"name":"OSX","client":{"name": "Apple", "clientId": 2}},+          {"name":"Orphan","client":null}+        ]|]+        { matchHeaders = [matchContentTypeJson] }++    it "requesting parent and specifying/renaming one key of the composite primary key" $ do+      get "/comments?select=*,users_tasks(userId:user_id)" `shouldRespondWith`+        [json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"userId": 2}}]|]+        { matchHeaders = [matchContentTypeJson] }+      get "/comments?select=*,users_tasks(taskId:task_id)" `shouldRespondWith`+        [json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"taskId": 6}}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "requesting parents and children while renaming them" $+      get "/projects?id=eq.1&select=myId:id, name, project_client:clients(*), project_tasks:tasks(id, name)" `shouldRespondWith`+        [json|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "requesting parents and filtering parent columns" $+      get "/projects?id=eq.1&select=id, name, clients(id)" `shouldRespondWith`+        [json|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|]++    it "rows with missing parents are included" $+      get "/projects?id=in.(1,5)&select=id,clients(id)" `shouldRespondWith`+        [json|[{"id":1,"clients":{"id":1}},{"id":5,"clients":null}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "rows with no children return [] instead of null" $+      get "/projects?id=in.(5)&select=id,tasks(id)" `shouldRespondWith`+        [json|[{"id":5,"tasks":[]}]|]++    it "requesting children 2 levels" $+      get "/clients?id=eq.1&select=id,projects(id,tasks(id))" `shouldRespondWith`+        [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "requesting many<->many relation" $+      get "/tasks?select=id,users(id)" `shouldRespondWith`+        [json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "requesting many<->many relation with rename" $+      get "/tasks?id=eq.1&select=id,theUsers:users(id)" `shouldRespondWith`+        [json|[{"id":1,"theUsers":[{"id":1},{"id":3}]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "requesting many<->many relation reverse" $+      get "/users?select=id,tasks(id)" `shouldRespondWith`+        [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 many<->many relation using composite key" $+      get "/files?filename=eq.autoexec.bat&project_id=eq.1&select=filename,users_tasks(user_id,task_id)" `shouldRespondWith`+        [json|[{"filename":"autoexec.bat","users_tasks":[{"user_id":1,"task_id":1},{"user_id":3,"task_id":1}]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "requesting data using many<->many relation defined by composite keys" $+      get "/users_tasks?user_id=eq.1&task_id=eq.1&select=user_id,files(filename,content)" `shouldRespondWith`+        [json|[{"user_id":1,"files":[{"filename":"command.com","content":"#include <unix.h>"},{"filename":"autoexec.bat","content":"@ECHO OFF"},{"filename":"README.md","content":"# make $$$!"}]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "requesting data using many<->many (composite keys) relation using hint" $+      get "/users_tasks?user_id=eq.1&task_id=eq.1&select=user_id,files!touched_files(filename,content)" `shouldRespondWith`+        [json|[{"user_id":1,"files":[{"filename":"command.com","content":"#include <unix.h>"},{"filename":"autoexec.bat","content":"@ECHO OFF"},{"filename":"README.md","content":"# make $$$!"}]}]|]+        { 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] }++    describe "computed columns" $ do+      it "computed column on table" $+        get "/items?id=eq.1&select=id,always_true" `shouldRespondWith`+          [json|[{"id":1,"always_true":true}]|]+          { matchHeaders = [matchContentTypeJson] }++      it "computed column on rpc" $+        get "/rpc/search?id=1&select=id,always_true" `shouldRespondWith`+          [json|[{"id":1,"always_true":true}]|]+          { matchHeaders = [matchContentTypeJson] }++      it "overloaded computed columns on both tables" $ do+        get "/items?id=eq.1&select=id,computed_overload" `shouldRespondWith`+          [json|[{"id":1,"computed_overload":true}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/items2?id=eq.1&select=id,computed_overload" `shouldRespondWith`+          [json|[{"id":1,"computed_overload":true}]|]+          { matchHeaders = [matchContentTypeJson] }++      it "overloaded computed column on rpc" $+        get "/rpc/search?id=1&select=id,computed_overload" `shouldRespondWith`+          [json|[{"id":1,"computed_overload":true}]|]+          { matchHeaders = [matchContentTypeJson] }++    when (actualPgVersion >= pgVersion110) $ do+      describe "partitioned tables embedding" $ do+        it "can request a table as parent from a partitioned table" $+          get "/car_models?name=in.(DeLorean,Murcielago)&select=name,year,car_brands(name)&order=name.asc" `shouldRespondWith`+            [json|+              [{"name":"DeLorean","year":1981,"car_brands":{"name":"DMC"}},+               {"name":"Murcielago","year":2001,"car_brands":{"name":"Lamborghini"}}] |]+            { matchHeaders = [matchContentTypeJson] }++        it "can request partitioned tables as children from a table" $+          get "/car_brands?select=name,car_models(name,year)&order=name.asc&car_models.order=name.asc" `shouldRespondWith`+            [json|+              [{"name":"DMC","car_models":[{"name":"DeLorean","year":1981}]},+               {"name":"Ferrari","car_models":[{"name":"F310-B","year":1997}]},+               {"name":"Lamborghini","car_models":[{"name":"Murcielago","year":2001},{"name":"Veneno","year":2013}]}] |]+            { matchHeaders = [matchContentTypeJson] }++        when (actualPgVersion >= pgVersion121) $ do+          it "can request tables as children from a partitioned table" $+            get "/car_models?name=in.(DeLorean,F310-B)&select=name,year,car_racers(name)&order=name.asc" `shouldRespondWith`+              [json|+                [{"name":"DeLorean","year":1981,"car_racers":[]},+                 {"name":"F310-B","year":1997,"car_racers":[{"name":"Michael Schumacher"}]}] |]+              { matchHeaders = [matchContentTypeJson] }++          it "can request a partitioned table as parent from a table" $+            get "/car_racers?select=name,car_models(name,year)&order=name.asc" `shouldRespondWith`+              [json|+                [{"name":"Alain Prost","car_models":null},+                 {"name":"Michael Schumacher","car_models":{"name":"F310-B","year":1997}}] |]+              { matchHeaders = [matchContentTypeJson] }++          it "can request partitioned tables as children from a partitioned table" $+            get "/car_models?name=in.(DeLorean,Murcielago,Veneno)&select=name,year,car_model_sales(date,quantity)&order=name.asc" `shouldRespondWith`+              [json|+                [{"name":"DeLorean","year":1981,"car_model_sales":[{"date":"2021-01-14","quantity":7},{"date":"2021-01-15","quantity":9}]},+                 {"name":"Murcielago","year":2001,"car_model_sales":[{"date":"2021-02-11","quantity":1},{"date":"2021-02-12","quantity":3}]},+                 {"name":"Veneno","year":2013,"car_model_sales":[]}] |]+              { matchHeaders = [matchContentTypeJson] }++          it "can request a partitioned table as parent from a partitioned table" $ do+            get "/car_model_sales?date=in.(2021-01-15,2021-02-11)&select=date,quantity,car_models(name,year)&order=date.asc" `shouldRespondWith`+              [json|+                [{"date":"2021-01-15","quantity":9,"car_models":{"name":"DeLorean","year":1981}},+                 {"date":"2021-02-11","quantity":1,"car_models":{"name":"Murcielago","year":2001}}] |]+              { matchHeaders = [matchContentTypeJson] }++          it "can request many to many relationships between partitioned tables ignoring the intermediate table partitions" $+            get "/car_models?select=name,year,car_dealers(name,city)&order=name.asc&limit=4" `shouldRespondWith`+              [json|+                [{"name":"DeLorean","year":1981,"car_dealers":[{"name":"Springfield Cars S.A.","city":"Springfield"}]},+                 {"name":"F310-B","year":1997,"car_dealers":[]},+                 {"name":"Murcielago","year":2001,"car_dealers":[{"name":"The Best Deals S.A.","city":"Franklin"}]},+                 {"name":"Veneno","year":2013,"car_dealers":[]}] |]+              { matchStatus  = 200+              , matchHeaders = [matchContentTypeJson]+              }++          it "cannot request partitions as children from a partitioned table" $+            get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith`+              [json|+                {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",+                 "message":"Could not find a relationship between car_models and car_model_sales_202101 in the schema cache"} |]+              { matchStatus  = 400+              , matchHeaders = [matchContentTypeJson]+              }++          it "cannot request a partitioned table as parent from a partition" $+            get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`+              [json|+                {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",+                 "message":"Could not find a relationship between car_model_sales_202101 and car_models in the schema cache"} |]+              { matchStatus  = 400+              , matchHeaders = [matchContentTypeJson]+              }++          it "cannot request a partition as parent from a partitioned table" $+            get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith`+              [json|+                {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",+                 "message":"Could not find a relationship between car_model_sales and car_models_default in the schema cache"} |]+              { matchStatus  = 400+              , matchHeaders = [matchContentTypeJson]+              }++          it "cannot request partitioned tables as children from a partition" $+            get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`+              [json|+                {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",+                 "message":"Could not find a relationship between car_models_default and car_model_sales in the schema cache"} |]+              { matchStatus  = 400+              , matchHeaders = [matchContentTypeJson]+              }++    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 detect fk relations through materialized views to tables in the public schema" $+        get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200++      it "can request two parents" $+        get "/articleStars?select=createdAt,article:articles(id),user:users(name)&limit=1"+          `shouldRespondWith`+            [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"id": 1},"user":{"name": "Angela Martin"}}]|]++      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:authors(name)&id=eq.5" `shouldRespondWith`+          [json|[ { "title": "Farenheit 451", "author": { "name": "Ray Bradbury" } } ]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/forties_books?select=title,author:authors(name)&limit=1" `shouldRespondWith`+          [json|[ { "title": "1984", "author": { "name": "George Orwell" } } ]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/fifties_books?select=title,author:authors(name)&limit=1" `shouldRespondWith`+          [json|[ { "title": "The Catcher in the Rye", "author": { "name": "J.D. Salinger" } } ]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/sixties_books?select=title,author:authors(name)&limit=1" `shouldRespondWith`+          [json|[ { "title": "To Kill a Mockingbird", "author": { "name": "Harper Lee" } } ]|]+          { matchHeaders = [matchContentTypeJson] }++      it "can detect fk relations through multiple views recursively when all views are in api schema" $ do+        get "/consumers_view_view?select=*,orders_view(*)" `shouldRespondWith` 200++      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 subselects in a function call" $+        get "/authors_have_book_in_decade2?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"}]} ]|]++      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:clients(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] }++      it "can embed a view that has a subselect containing a select in a where" $+        get "/authors_w_entities?select=name,entities,books(title)&id=eq.1" `shouldRespondWith`+          [json| [{"name":"George Orwell","entities":[3, 4],"books":[{"title":"1984"}]}] |]+          { matchHeaders = [matchContentTypeJson] }++    describe "aliased embeds" $ do+      it "works with child relation" $+        get "/space?select=id,zones:zone(id,name),stores:zone(id,name)&zones.zone_type_id=eq.2&stores.zone_type_id=eq.3" `shouldRespondWith`+          [json|[+            { "id":1,+              "zones": [ {"id":1,"name":"zone 1"}, {"id":2,"name":"zone 2"}],+              "stores": [ {"id":3,"name":"store 3"}, {"id":4,"name":"store 4"}]}+          ]|] { matchHeaders = [matchContentTypeJson] }++      it "works with many to many relation" $+        get "/users?select=id,designTasks:tasks(id,name),codeTasks:tasks(id,name)&designTasks.name=like.*Design*&codeTasks.name=like.*Code*" `shouldRespondWith`+          [json|[+             { "id":1,+               "designTasks":[ { "id":1, "name":"Design w7" }, { "id":3, "name":"Design w10" } ],+               "codeTasks":[ { "id":2, "name":"Code w7" }, { "id":4, "name":"Code w10" } ] },+             { "id":2,+               "designTasks":[ { "id":5, "name":"Design IOS" }, { "id":7, "name":"Design OSX" } ],+               "codeTasks":[ { "id":6, "name":"Code IOS" } ] },+             { "id":3,+               "designTasks":[ { "id":1, "name":"Design w7" }, { "id":5, "name":"Design IOS" } ],+               "codeTasks":[ ] }+          ]|] { matchHeaders = [matchContentTypeJson] }++      it "works with an aliased child plus non aliased child" $+        get "/projects?select=id,name,designTasks:tasks(name,users(id,name))&designTasks.name=like.*Design*&designTasks.users.id=in.(1,2)" `shouldRespondWith`+          [json|[+            {+              "id":1, "name":"Windows 7",+              "designTasks":[ { "name":"Design w7", "users":[ { "id":1, "name":"Angela Martin" } ] } ] },+            {+              "id":2, "name":"Windows 10",+              "designTasks":[ { "name":"Design w10", "users":[ { "id":1, "name":"Angela Martin" } ] } ] },+            {+              "id":3, "name":"IOS",+              "designTasks":[ { "name":"Design IOS", "users":[ { "id":2, "name":"Michael Scott" } ] } ] },+            {+              "id":4, "name":"OSX",+              "designTasks":[ { "name":"Design OSX", "users":[ { "id":2, "name":"Michael Scott" } ] } ] },+            {+              "id":5, "name":"Orphan",+              "designTasks":[ ] }+          ]|] { matchHeaders = [matchContentTypeJson] }++      it "works with two aliased children embeds plus and/or" $+        get "/entities?select=id,children:child_entities(id,gChildren:grandchild_entities(id))&children.and=(id.in.(1,2,3))&children.gChildren.or=(id.eq.1,id.eq.2)" `shouldRespondWith`+          [json|[+            { "id":1,+              "children":[+                {"id":1,"gChildren":[{"id":1}, {"id":2}]},+                {"id":2,"gChildren":[]}]},+            { "id":2,+              "children":[+                {"id":3,"gChildren":[]}]},+            { "id":3,"children":[]},+            { "id":4,"children":[]}+          ]|] { matchHeaders = [matchContentTypeJson] }++  describe "ordering response" $ do+    it "by a column asc" $+      get "/items?id=lte.2&order=id.asc"+        `shouldRespondWith` [json| [{"id":1},{"id":2}] |]+        { matchStatus  = 200+        , matchHeaders = ["Content-Range" <:> "0-1/*"]+        }+++    it "by a column desc" $+      get "/items?id=lte.2&order=id.desc"+        `shouldRespondWith` [json| [{"id":2},{"id":1}] |]+        { matchStatus  = 200+        , matchHeaders = ["Content-Range" <:> "0-1/*"]+        }++    it "by a column with nulls first" $+      get "/no_pk?order=a.nullsfirst"+        `shouldRespondWith` [json| [{"a":null,"b":null},+                              {"a":"1","b":"0"},+                              {"a":"2","b":"0"}+                              ] |]+        { matchStatus = 200+        , matchHeaders = ["Content-Range" <:> "0-2/*"]+        }++    it "by a column asc with nulls last" $+      get "/no_pk?order=a.asc.nullslast"+        `shouldRespondWith` [json| [{"a":"1","b":"0"},+                              {"a":"2","b":"0"},+                              {"a":null,"b":null}] |]+        { matchStatus = 200+        , matchHeaders = ["Content-Range" <:> "0-2/*"]+        }++    it "by a column desc with nulls first" $+      get "/no_pk?order=a.desc.nullsfirst"+        `shouldRespondWith` [json| [{"a":null,"b":null},+                              {"a":"2","b":"0"},+                              {"a":"1","b":"0"}] |]+        { matchStatus = 200+        , matchHeaders = ["Content-Range" <:> "0-2/*"]+        }++    it "by a column desc with nulls last" $+      get "/no_pk?order=a.desc.nullslast"+        `shouldRespondWith` [json| [{"a":"2","b":"0"},+                              {"a":"1","b":"0"},+                              {"a":null,"b":null}] |]+        { matchStatus = 200+        , matchHeaders = ["Content-Range" <:> "0-2/*"]+        }++    it "by two columns with nulls and direction specified" $+      get "/projects?select=client_id,id,name&order=client_id.desc.nullslast,id.desc"+        `shouldRespondWith` [json|+          [{"client_id":2,"id":4,"name":"OSX"},+           {"client_id":2,"id":3,"name":"IOS"},+           {"client_id":1,"id":2,"name":"Windows 10"},+           {"client_id":1,"id":1,"name":"Windows 7"},+           {"client_id":null,"id":5,"name":"Orphan"}]+        |]+        { matchStatus  = 200+        , matchHeaders = ["Content-Range" <:> "0-4/*"]+        }++    it "by a column with no direction or nulls specified" $+      get "/items?id=lte.2&order=id"+        `shouldRespondWith` [json| [{"id":1},{"id":2}] |]+        { matchStatus  = 200+        , matchHeaders = ["Content-Range" <:> "0-1/*"]+        }++    it "without other constraints" $+      get "/items?order=id.asc" `shouldRespondWith` 200++    it "ordering embeded entities" $+      get "/projects?id=eq.1&select=id, name, tasks(id, name)&tasks.order=name.asc" `shouldRespondWith`+        [json|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "ordering embeded entities with alias" $+      get "/projects?id=eq.1&select=id, name, the_tasks:tasks(id, name)&tasks.order=name.asc" `shouldRespondWith`+        [json|[{"id":1,"name":"Windows 7","the_tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "ordering embeded entities, two levels" $+      get "/projects?id=eq.1&select=id, name, tasks(id, name, users(id, name))&tasks.order=name.asc&tasks.users.order=name.desc" `shouldRespondWith`+        [json|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7","users":[{"id":1,"name":"Angela Martin"}]},{"id":1,"name":"Design w7","users":[{"id":3,"name":"Dwight Schrute"},{"id":1,"name":"Angela Martin"}]}]}]|]+        { matchHeaders = [matchContentTypeJson] }++    it "ordering embeded parents does not break things" $+      get "/projects?id=eq.1&select=id, name, clients(id, name)&clients.order=name.asc" `shouldRespondWith`+        [json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"}}]|]++    context "order syntax errors" $ do+      it "gives meaningful error messages when asc/desc/nulls{first,last} are misspelled" $ do+        get "/items?order=id.ac" `shouldRespondWith`+          [json|{"details":"unexpected \"c\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.ac)\" (line 1, column 4)"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson]+          }+        get "/items?order=id.descc" `shouldRespondWith`+          [json|{"details":"unexpected 'c' expecting delimiter (.), \",\" or end of input","message":"\"failed to parse order (id.descc)\" (line 1, column 8)"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson]+          }+        get "/items?order=id.nulsfist" `shouldRespondWith`+          [json|{"details":"unexpected \"n\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.nulsfist)\" (line 1, column 4)"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson]+          }+        get "/items?order=id.nullslasttt" `shouldRespondWith`+          [json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.nullslasttt)\" (line 1, column 13)"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson]+          }+        get "/items?order=id.smth34" `shouldRespondWith`+          [json|{"details":"unexpected \"s\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.smth34)\" (line 1, column 4)"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson]+          }++      it "gives meaningful error messages when nulls{first,last} are misspelled after asc/desc" $ do+        get "/items?order=id.asc.nlsfst" `shouldRespondWith`+          [json|{"details":"unexpected \"l\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.nlsfst)\" (line 1, column 8)"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson]+          }+        get "/items?order=id.asc.nullslasttt" `shouldRespondWith`+          [json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.asc.nullslasttt)\" (line 1, column 17)"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson]+          }+        get "/items?order=id.asc.smth34" `shouldRespondWith`+          [json|{"details":"unexpected \"s\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.smth34)\" (line 1, column 8)"}|]+          { matchStatus  = 400+          , matchHeaders = [matchContentTypeJson]+          }++  describe "Accept headers" $ do+    it "should respond an unknown accept type with 415" $+      request methodGet "/simple_pk"+              (acceptHdrs "text/unknowntype") ""+        `shouldRespondWith`+        [json|{"message":"None of these Content-Types are available: text/unknowntype"}|]+        { matchStatus  = 415+        , matchHeaders = [matchContentTypeJson]+        }++    it "should respond correctly to */* in accept header" $+      request methodGet "/simple_pk"+              (acceptHdrs "*/*") ""+        `shouldRespondWith` 200++    it "*/* should rescue an unknown type" $+      request methodGet "/simple_pk"+              (acceptHdrs "text/unknowntype, */*") ""+        `shouldRespondWith` 200++    it "specific available preference should override */*" $ do+      r <- request methodGet "/simple_pk"+              (acceptHdrs "text/csv, */*") ""+      liftIO $ do+        let respHeaders = simpleHeaders r+        respHeaders `shouldSatisfy` matchHeader+          "Content-Type" "text/csv; charset=utf-8"++    it "honors client preference even when opposite of server preference" $ do+      r <- request methodGet "/simple_pk"+              (acceptHdrs "text/csv, application/json") ""+      liftIO $ do+        let respHeaders = simpleHeaders r+        respHeaders `shouldSatisfy` matchHeader+          "Content-Type" "text/csv; charset=utf-8"++    it "should respond correctly to multiple types in accept header" $+      request methodGet "/simple_pk"+              (acceptHdrs "text/unknowntype, text/csv") ""+        `shouldRespondWith` 200++    it "should respond with CSV to 'text/csv' request" $+      request methodGet "/simple_pk"+              (acceptHdrs "text/csv; version=1") ""+        `shouldRespondWith` "k,extra\nxyyx,u\nxYYx,v"+        { matchStatus  = 200+        , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]+        }++  describe "Canonical location" $ do+    it "Sets Content-Location with alphabetized params" $+      get "/no_pk?b=eq.1&a=eq.1"+        `shouldRespondWith` "[]"+        { matchStatus  = 200+        , matchHeaders = ["Content-Location" <:> "/no_pk?a=eq.1&b=eq.1"]+        }++    it "Omits question mark when there are no params" $ do+      r <- get "/simple_pk"+      liftIO $ do+        let respHeaders = simpleHeaders r+        respHeaders `shouldSatisfy` matchHeader+          "Content-Location" "/simple_pk"++  describe "weird requests" $ do+    it "can query as normal" $ do+      get "/Escap3e;" `shouldRespondWith`+        [json| [{"so6meIdColumn":1},{"so6meIdColumn":2},{"so6meIdColumn":3},{"so6meIdColumn":4},{"so6meIdColumn":5}] |]+        { matchHeaders = [matchContentTypeJson] }+      get "/ghostBusters" `shouldRespondWith`+        [json| [{"escapeId":1},{"escapeId":3},{"escapeId":5}] |]+        { matchHeaders = [matchContentTypeJson] }++    it "fails if an operator is not given" $+      get "/ghostBusters?id=0" `shouldRespondWith` [json| {"details":"unexpected \"0\" expecting \"not\" or operator (eq, gt, ...)","message":"\"failed to parse filter (0)\" (line 1, column 1)"} |]+        { matchStatus  = 400+        , matchHeaders = [matchContentTypeJson]+        }++    it "will embed a collection" $+      get "/Escap3e;?select=ghostBusters(*)" `shouldRespondWith`+        [json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":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] }++  context "binary output" $ do+    it "can query if a single column is selected" $+      request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") ""+        `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"+        { matchStatus = 200+        , matchHeaders = ["Content-Type" <:> "application/octet-stream"]+        }++    it "can get raw output with Accept: text/plain" $+      request methodGet "/projects?select=name&id=eq.1" (acceptHdrs "text/plain") ""+        `shouldRespondWith` "Windows 7"+        { matchStatus = 200+        , matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]+        }++    it "fails if a single column is not selected" $ do+      request methodGet "/images?select=img,name&name=eq.A.png" (acceptHdrs "application/octet-stream") ""+        `shouldRespondWith`+          [json| {"message":"application/octet-stream requested but more than one column was selected"} |]+          { matchStatus = 406 }++      request methodGet "/images?select=*&name=eq.A.png"+          (acceptHdrs "application/octet-stream")+          ""+        `shouldRespondWith`+          [json| {"message":"application/octet-stream requested but more than one column was selected"} |]+          { matchStatus = 406 }++      request methodGet "/images?name=eq.A.png"+          (acceptHdrs "application/octet-stream")+          ""+        `shouldRespondWith`+          [json| {"message":"application/octet-stream requested but more than one column was selected"} |]+          { matchStatus = 406 }++    it "concatenates results if more than one row is returned" $+      request methodGet "/images_base64?select=img&name=in.(A.png,B.png)" (acceptHdrs "application/octet-stream") ""+        `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="+        { matchStatus = 200+        , matchHeaders = ["Content-Type" <:> "application/octet-stream"]+        }++  describe "values with quotes in IN and NOT IN" $ do+    it "succeeds when only quoted values are present" $ do+      get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\")" `shouldRespondWith`+        [json| [{"name":"Hebdon, John"}] |]+        { matchHeaders = [matchContentTypeJson] }+      get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")" `shouldRespondWith`+        [json| [{"name":"Hebdon, John"},{"name":"Williams, Mary"},{"name":"Smith, Joseph"}] |]+        { matchHeaders = [matchContentTypeJson] }+      get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")&limit=3" `shouldRespondWith`+        [json| [ { "name": "David White" }, { "name": "Larry Thompson" }, { "name": "Double O Seven(007)" }] |]+        { matchHeaders = [matchContentTypeJson] }++    it "succeeds w/ and w/o quoted values" $ do+      get "/w_or_wo_comma_names?name=in.(David White,\"Hebdon, John\")" `shouldRespondWith`+        [json| [{"name":"Hebdon, John"},{"name":"David White"}] |]+        { matchHeaders = [matchContentTypeJson] }+      get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",Larry Thompson,\"Smith, Joseph\")&limit=3" `shouldRespondWith`+        [json| [ { "name": "Williams, Mary" }, { "name": "David White" }, { "name": "Double O Seven(007)" }] |]+        { matchHeaders = [matchContentTypeJson] }+      get "/w_or_wo_comma_names?name=in.(\"Double O Seven(007)\")" `shouldRespondWith`+        [json| [{"name":"Double O Seven(007)"}] |]+        { matchHeaders = [matchContentTypeJson] }++    context "escaped chars" $ do+      it "accepts escaped double quotes" $+        get "/w_or_wo_comma_names?name=in.(\"Double\\\"Quote\\\"McGraw\\\"\")" `shouldRespondWith`+          [json| [ { "name": "Double\"Quote\"McGraw\"" } ] |]+          { matchHeaders = [matchContentTypeJson] }++      it "accepts escaped backslashes" $ do+        get "/w_or_wo_comma_names?name=in.(\"\\\\\")" `shouldRespondWith`+          [json| [{ "name": "\\" }] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/w_or_wo_comma_names?name=in.(\"/\\\\Slash/\\\\Beast/\\\\\")" `shouldRespondWith`+          [json| [ { "name": "/\\Slash/\\Beast/\\" } ] |]+          { matchHeaders = [matchContentTypeJson] }++      it "passes any escaped char as the same char" $+        get "/w_or_wo_comma_names?name=in.(\"D\\a\\vid W\\h\\ite\")" `shouldRespondWith`+          [json| [{ "name": "David White" }] |]+          { matchHeaders = [matchContentTypeJson] }++  describe "IN values without quotes" $ do+    it "accepts single double quotes as values" $ do+      get "/w_or_wo_comma_names?name=in.(\")" `shouldRespondWith`+        [json| [{ "name": "\"" }] |]+        { matchHeaders = [matchContentTypeJson] }+      get "/w_or_wo_comma_names?name=in.(Double\"Quote\"McGraw\")" `shouldRespondWith`+        [json| [ { "name": "Double\"Quote\"McGraw\"" } ] |]+        { matchHeaders = [matchContentTypeJson] }++    it "accepts backslashes as values" $ do+      get "/w_or_wo_comma_names?name=in.(\\)" `shouldRespondWith`+        [json| [{ "name": "\\" }] |]+        { matchHeaders = [matchContentTypeJson] }+      get "/w_or_wo_comma_names?name=in.(/\\Slash/\\Beast/\\)" `shouldRespondWith`+        [json| [ { "name": "/\\Slash/\\Beast/\\" } ] |]+        { matchHeaders = [matchContentTypeJson] }++  describe "IN and NOT IN empty set" $ do+    context "returns an empty result for IN when no value is present" $ do+      it "works for integer" $+        get "/items_with_different_col_types?int_data=in.()" `shouldRespondWith`+          [json| [] |] { matchHeaders = [matchContentTypeJson] }+      it "works for text" $+        get "/items_with_different_col_types?text_data=in.()" `shouldRespondWith`+          [json| [] |] { matchHeaders = [matchContentTypeJson] }+      it "works for bool" $+        get "/items_with_different_col_types?bool_data=in.()" `shouldRespondWith`+          [json| [] |] { matchHeaders = [matchContentTypeJson] }+      it "works for bytea" $+        get "/items_with_different_col_types?bin_data=in.()" `shouldRespondWith`+          [json| [] |] { matchHeaders = [matchContentTypeJson] }+      it "works for char" $+        get "/items_with_different_col_types?char_data=in.()" `shouldRespondWith`+          [json| [] |] { matchHeaders = [matchContentTypeJson] }+      it "works for date" $+        get "/items_with_different_col_types?date_data=in.()" `shouldRespondWith`+          [json| [] |] { matchHeaders = [matchContentTypeJson] }+      it "works for real" $+        get "/items_with_different_col_types?real_data=in.()" `shouldRespondWith`+          [json| [] |] { matchHeaders = [matchContentTypeJson] }+      it "works for time" $+        get "/items_with_different_col_types?time_data=in.()" `shouldRespondWith`+          [json| [] |] { matchHeaders = [matchContentTypeJson] }++    it "returns all results for not.in when no value is present" $+      get "/items_with_different_col_types?int_data=not.in.()&select=int_data" `shouldRespondWith`+        [json| [{int_data: 1}] |] { matchHeaders = [matchContentTypeJson] }++    it "returns an empty result ignoring spaces" $+      get "/items_with_different_col_types?int_data=in.(    )" `shouldRespondWith`+        [json| [] |] { matchHeaders = [matchContentTypeJson] }++    it "only returns an empty result set if the in value is empty" $+      get "/items_with_different_col_types?int_data=in.( ,3,4)"+        `shouldRespondWith` (+        if actualPgVersion >= pgVersion121 then+        [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for type integer: \"\""} |]+        else+        [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"\""} |]+                            )+        { matchStatus = 400+        , matchHeaders = [matchContentTypeJson]+        }++  describe "Embedding when column name = table name" $ do+    it "works with child embeds" $+      get "/being?select=*,descendant(*)&limit=1" `shouldRespondWith`+        [json|[{"being":1,"descendant":[{"descendant":1,"being":1},{"descendant":2,"being":1},{"descendant":3,"being":1}]}]|]+        { matchHeaders = [matchContentTypeJson] }+    it "works with many to many embeds" $+      get "/being?select=*,part(*)&limit=1" `shouldRespondWith`+        [json|[{"being":1,"part":[{"part":1}]}]|]+        { matchHeaders = [matchContentTypeJson] }++  describe "Foreign table" $ do+    it "can be queried by using regular filters" $+      get "/projects_dump?id=in.(1,2,3)" `shouldRespondWith`+        [json| [{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}, {"id":3,"name":"IOS","client_id":2}]|]+        { matchHeaders = [matchContentTypeJson] }+    it "can be queried with select, order and limit" $+      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++  context "VIEW that has a source FK based on a UNIQUE key" $+    it "can be embedded" $+      get "/referrals?select=site,link:pages(url)" `shouldRespondWith`+        [json| [+         {"site":"github.com",     "link":{"url":"http://postgrest.org/en/v6.0/api.html"}},+         {"site":"hub.docker.com", "link":{"url":"http://postgrest.org/en/v6.0/admin.html"}}+        ]|]+        { matchHeaders = [matchContentTypeJson] }++  it "shouldn't produce a Content-Profile header since only a single schema is exposed" $ do+    r <- get "/items"+    liftIO $ do+      let respHeaders = simpleHeaders r+      respHeaders `shouldSatisfy` noProfileHeader
+ test/spec/Feature/RangeSpec.hs view
@@ -0,0 +1,352 @@+module Feature.RangeSpec where++import qualified Data.ByteString.Lazy as BL++import Network.Wai      (Application)+import Network.Wai.Test (SResponse (simpleHeaders, simpleStatus))++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude  hiding (get)+import SpecHelper++defaultRange :: BL.ByteString+defaultRange = [json| { "min": 0, "max": 15 } |]++emptyRange :: BL.ByteString+emptyRange = [json| { "min": 2, "max": 2 } |]++spec :: SpecWith ((), Application)+spec = do+  describe "POST /rpc/getitemrange" $ do+    context "without range headers" $ do+      context "with response under server size limit" $+        it "returns whole range with status 200" $+           post "/rpc/getitemrange" defaultRange `shouldRespondWith` 200++      context "when I don't want the count" $ do+        it "returns range Content-Range with */* for empty range" $+          request methodPost "/rpc/getitemrange" [] emptyRange+            `shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}++        it "returns range Content-Range with range/*" $+          post "/rpc/getitemrange?order=id"+              defaultRange+            `shouldRespondWith`+              [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]+              { matchHeaders = ["Content-Range" <:> "0-14/*"] }++    context "with range headers" $ do+      context "of acceptable range" $ do+        it "succeeds with partial content" $ do+          r <- request methodPost  "/rpc/getitemrange"+                       (rangeHdrs $ ByteRangeFromTo 0 1) defaultRange+          liftIO $ do+            simpleHeaders r `shouldSatisfy`+              matchHeader "Content-Range" "0-1/*"+            simpleStatus r `shouldBe` ok200++        it "understands open-ended ranges" $+          request methodPost "/rpc/getitemrange"+                  (rangeHdrs $ ByteRangeFrom 0) defaultRange+            `shouldRespondWith` 200++        it "returns an empty body when there are no results" $+          request methodPost "/rpc/getitemrange"+                  (rangeHdrs $ ByteRangeFromTo 0 1) emptyRange+            `shouldRespondWith` "[]"+            { matchStatus  = 200+            , matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "allows one-item requests" $ do+          r <- request methodPost  "/rpc/getitemrange"+                       (rangeHdrs $ ByteRangeFromTo 0 0) defaultRange+          liftIO $ do+            simpleHeaders r `shouldSatisfy`+              matchHeader "Content-Range" "0-0/*"+            simpleStatus r `shouldBe` ok200++        it "handles ranges beyond collection length via truncation" $ do+          r <- request methodPost  "/rpc/getitemrange"+                       (rangeHdrs $ ByteRangeFromTo 10 100) defaultRange+          liftIO $ do+            simpleHeaders r `shouldSatisfy`+              matchHeader "Content-Range" "10-14/*"+            simpleStatus r `shouldBe` ok200++      context "of invalid range" $ do+        it "fails with 416 for offside range" $+          request methodPost  "/rpc/getitemrange"+                  (rangeHdrs $ ByteRangeFromTo 1 0) emptyRange+            `shouldRespondWith` 416++        it "refuses a range with nonzero start when there are no items" $+          request methodPost "/rpc/getitemrange"+                  (rangeHdrsWithCount $ ByteRangeFromTo 1 2) emptyRange+            `shouldRespondWith` "[]"+            { matchStatus  = 416+            , matchHeaders = ["Content-Range" <:> "*/0"]+            }++        it "refuses a range requesting start past last item" $+          request methodPost "/rpc/getitemrange"+                  (rangeHdrsWithCount $ ByteRangeFromTo 100 199) defaultRange+            `shouldRespondWith` "[]"+            { matchStatus  = 416+            , matchHeaders = ["Content-Range" <:> "*/15"]+            }++  describe "GET /items" $ do+    context "without range headers" $ do+      context "with response under server size limit" $+        it "returns whole range with status 200" $+          get "/items" `shouldRespondWith` 200++      context "when I don't want the count" $ do+        it "returns range Content-Range with /*" $+          request methodGet "/menagerie"+                  [("Prefer", "count=none")] ""+            `shouldRespondWith` "[]"+            { matchStatus  = 200+            , matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "returns range Content-Range with range/*" $+          request methodGet "/items?order=id"+                  [("Prefer", "count=none")] ""+            `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]+            { matchHeaders = ["Content-Range" <:> "0-14/*"] }++        it "returns range Content-Range with range/* even using other filters" $+          request methodGet "/items?id=eq.1&order=id"+                  [("Prefer", "count=none")] ""+            `shouldRespondWith` [json| [{"id":1}] |]+            { matchHeaders = ["Content-Range" <:> "0-0/*"] }++    context "with limit/offset parameters" $ do+      it "no parameters return everything" $+        get "/items?select=id&order=id.asc"+          `shouldRespondWith`+          [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "0-14/*"]+          }+      it "top level limit with parameter" $+        get "/items?select=id&order=id.asc&limit=3"+          `shouldRespondWith` [json|[{"id":1},{"id":2},{"id":3}]|]+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "0-2/*"]+          }+      it "headers override get parameters" $+        request methodGet  "/items?select=id&order=id.asc&limit=3"+                     (rangeHdrs $ ByteRangeFromTo 0 1) ""+          `shouldRespondWith` [json|[{"id":1},{"id":2}]|]+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "0-1/*"]+          }++      it "limit works on all levels" $+        get "/clients?select=id,projects(id,tasks(id))&order=id.asc&limit=1&projects.order=id.asc&projects.limit=2&projects.tasks.order=id.asc&projects.tasks.limit=1"+          `shouldRespondWith`+          [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1}]},{"id":2,"tasks":[{"id":3}]}]}]|]+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "0-0/*"]+          }++      it "limit and offset works on first level" $ do+        get "/items?select=id&order=id.asc&limit=3&offset=2"+          `shouldRespondWith` [json|[{"id":3},{"id":4},{"id":5}]|]+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "2-4/*"]+          }+        request methodHead "/items?select=id&order=id.asc&limit=3&offset=2" [] mempty+          `shouldRespondWith` ""+          { matchStatus  = 200+          , matchHeaders = ["Content-Range" <:> "2-4/*"]+          }++      it "succeeds if offset equals 0 as a no-op" $+        get "/items?select=id&offset=0&order=id"+          `shouldRespondWith`+            [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]+            { matchHeaders = ["Content-Range" <:> "0-14/*"] }++      it "succeeds if offset is negative as a no-op" $+        get "/items?select=id&offset=-4&order=id"+          `shouldRespondWith`+            [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]+            { matchHeaders = ["Content-Range" <:> "0-14/*"] }++      it "fails if limit equals 0" $+        get "/items?select=id&limit=0"+          `shouldRespondWith` [json|{"message":"HTTP Range error"}|]+          { matchStatus  = 416+          , matchHeaders = [matchContentTypeJson]+          }++      it "fails if limit is negative" $+        get "/items?select=id&limit=-1"+          `shouldRespondWith` [json|{"message":"HTTP Range error"}|]+          { matchStatus  = 416+          , matchHeaders = [matchContentTypeJson]+          }++    context "when count=planned" $ do+      it "obtains a filtered range" $ do+        request methodGet "/items?select=id&id=gt.8"+            [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            [json|[{"id":9}, {"id":10}, {"id":11}, {"id":12}, {"id":13}, {"id":14}, {"id":15}]|]+            { matchStatus  = 206+            , matchHeaders = ["Content-Range" <:> "0-6/8"]+            }++        request methodGet "/child_entities?select=id&id=gt.3"+            [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            [json|[{"id":4}, {"id":5}, {"id":6}]|]+            { matchStatus  = 206+            , matchHeaders = ["Content-Range" <:> "0-2/4"]+            }++        request methodGet "/getallprojects_view?select=id&id=lt.3"+            [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            [json|[{"id":1}, {"id":2}]|]+            { matchStatus  = 206+            , matchHeaders = ["Content-Range" <:> "0-1/673"]+            }++      it "obtains the full range" $ do+        request methodHead "/items"+            [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            ""+            { matchStatus  = 200+            , matchHeaders = ["Content-Range" <:> "0-14/15"]+            }++        request methodHead "/child_entities"+            [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            ""+            { matchStatus  = 200+            , matchHeaders = ["Content-Range" <:> "0-5/6"]+            }++        request methodHead "/getallprojects_view"+            [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            ""+            { matchStatus  = 206+            , matchHeaders = ["Content-Range" <:> "0-4/2019"]+            }++      it "ignores limit/offset on the planned count" $ do+        request methodHead "/items?limit=2&offset=3"+            [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            ""+            { matchStatus  = 206+            , matchHeaders = ["Content-Range" <:> "3-4/15"]+            }++        request methodHead "/child_entities?limit=2"+           [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            ""+            { matchStatus  = 206+            , matchHeaders = ["Content-Range" <:> "0-1/6"]+            }++        request methodHead "/getallprojects_view?limit=2"+            [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            ""+            { matchStatus  = 206+            , matchHeaders = ["Content-Range" <:> "0-1/2019"]+            }++      it "works with two levels" $+        request methodHead "/child_entities?select=*,entities(*)"+            [("Prefer", "count=planned")]+            ""+          `shouldRespondWith`+            ""+            { matchStatus  = 200+            , matchHeaders = ["Content-Range" <:> "0-5/6"]+            }++    context "with range headers" $ do+      context "of acceptable range" $ do+        it "succeeds with partial content" $ do+          r <- request methodGet  "/items"+                       (rangeHdrs $ ByteRangeFromTo 0 1) ""+          liftIO $ do+            simpleHeaders r `shouldSatisfy`+              matchHeader "Content-Range" "0-1/*"+            simpleStatus r `shouldBe` ok200++        it "understands open-ended ranges" $+          request methodGet "/items"+                  (rangeHdrs $ ByteRangeFrom 0) ""+            `shouldRespondWith` 200++        it "returns an empty body when there are no results" $+          request methodGet "/menagerie"+                  (rangeHdrs $ ByteRangeFromTo 0 1) ""+            `shouldRespondWith` "[]"+            { matchStatus  = 200+            , matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "allows one-item requests" $ do+          r <- request methodGet  "/items"+                       (rangeHdrs $ ByteRangeFromTo 0 0) ""+          liftIO $ do+            simpleHeaders r `shouldSatisfy`+              matchHeader "Content-Range" "0-0/*"+            simpleStatus r `shouldBe` ok200++        it "handles ranges beyond collection length via truncation" $ do+          r <- request methodGet  "/items"+                       (rangeHdrs $ ByteRangeFromTo 10 100) ""+          liftIO $ do+            simpleHeaders r `shouldSatisfy`+              matchHeader "Content-Range" "10-14/*"+            simpleStatus r `shouldBe` ok200++      context "of invalid range" $ do+        it "fails with 416 for offside range" $+          request methodGet  "/items"+                  (rangeHdrs $ ByteRangeFromTo 1 0) ""+            `shouldRespondWith` 416++        it "refuses a range with nonzero start when there are no items" $+          request methodGet "/menagerie"+                  (rangeHdrsWithCount $ ByteRangeFromTo 1 2) ""+            `shouldRespondWith` "[]"+            { matchStatus  = 416+            , matchHeaders = ["Content-Range" <:> "*/0"]+            }++        it "refuses a range requesting start past last item" $+          request methodGet "/items"+                  (rangeHdrsWithCount $ ByteRangeFromTo 100 199) ""+            `shouldRespondWith` "[]"+            { matchStatus  = 416+            , matchHeaders = ["Content-Range" <:> "*/15"]+            }
+ test/spec/Feature/RawOutputTypesSpec.hs view
@@ -0,0 +1,33 @@+module Feature.RawOutputTypesSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude+import SpecHelper (acceptHdrs)++spec :: SpecWith ((), Application)+spec = describe "When raw-media-types config variable is missing or left empty" $ do+  let firefoxAcceptHdrs = acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"+      chromeAcceptHdrs = acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"+  it "responds json to a GET request with Firefox Accept headers" $+    request methodGet "/items?id=eq.1" firefoxAcceptHdrs ""+      `shouldRespondWith` [json| [{"id":1}] |]+        { matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }+  it "responds json to a GET request with Chrome Accept headers" $+    request methodGet "/items?id=eq.1" chromeAcceptHdrs ""+      `shouldRespondWith` [json| [{"id":1}] |]+        { matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }++  it "responds json to a GET request to RPC with Firefox Accept headers" $+    request methodGet "/rpc/get_projects_below?id=3" firefoxAcceptHdrs ""+      `shouldRespondWith` [json|[{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}]|]+        { matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }+  it "responds json to a GET request to RPC with Chrome Accept headers" $+    request methodGet "/rpc/get_projects_below?id=3" chromeAcceptHdrs ""+      `shouldRespondWith` [json|[{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}]|]+        { matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }
+ test/spec/Feature/RollbackSpec.hs view
@@ -0,0 +1,260 @@+module Feature.RollbackSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude hiding (get)++-- two helpers functions to make sure that each test can setup and cleanup properly++-- creates Item to work with for PATCH and DELETE+postItem =+  request methodPost "/items"+      [("Prefer", "tx=commit"), ("Prefer", "resolution=ignore-duplicates")]+      [json|{"id":0}|]+    `shouldRespondWith`+      ""+      { matchStatus  = 201 }++-- removes Items left over from POST, PUT, and PATCH+deleteItems =+  request methodDelete "/items?id=lte.0"+      [("Prefer", "tx=commit")]+      ""+    `shouldRespondWith`+      ""+      { matchStatus  = 204 }++preferDefault  = [("Prefer", "return=representation")]+preferCommit   = [("Prefer", "return=representation"), ("Prefer", "tx=commit")]+preferRollback = [("Prefer", "return=representation"), ("Prefer", "tx=rollback")]++withoutPreferenceApplied      = []+withPreferenceCommitApplied   = [ "Preference-Applied" <:> "tx=commit" ]+withPreferenceRollbackApplied = [ "Preference-Applied" <:> "tx=rollback" ]++shouldRespondToReads reqHeaders respHeaders = do+  it "responds to GET" $ do+    request methodGet "/items?id=eq.1"+        reqHeaders+        ""+      `shouldRespondWith`+        [json|[{"id":1}]|]+        { matchHeaders = respHeaders }++  it "responds to HEAD" $ do+    request methodHead "/items?id=eq.1"+        reqHeaders+        ""+      `shouldRespondWith`+        ""+        { matchHeaders = respHeaders }++  it "responds to GET on RPC" $ do+    request methodGet "/rpc/search?id=1"+        reqHeaders+        ""+      `shouldRespondWith`+        [json|[{"id":1}]|]+        { matchHeaders = respHeaders }++  it "responds to POST on RPC" $ do+    request methodPost "/rpc/search"+        reqHeaders+        [json|{"id":1}|]+      `shouldRespondWith`+        [json|[{"id":1}]|]+        { matchHeaders = respHeaders }++shouldRaiseExceptions reqHeaders respHeaders = do+  it "raises immediate constraints" $ do+    request methodPost "/rpc/raise_constraint"+        reqHeaders+        ""+      `shouldRespondWith`+        [json|{+          "hint":null,+          "details":"Key (col)=(1) already exists.",+          "code":"23505",+          "message":"duplicate key value violates unique constraint \"deferrable_unique_constraint_col_key\""+        }|]+        { matchStatus = 409+        , matchHeaders = respHeaders }++  it "raises deferred constraints" $ do+    request methodPost "/rpc/raise_constraint"+        reqHeaders+        [json|{"deferred": true}|]+      `shouldRespondWith`+        [json|{+          "hint":null,+          "details":"Key (col)=(1) already exists.",+          "code":"23505",+          "message":"duplicate key value violates unique constraint \"deferrable_unique_constraint_col_key\""+        }|]+        { matchStatus = 409+        , matchHeaders = respHeaders }++shouldPersistMutations reqHeaders respHeaders = do+  it "does persist post" $ do+    request methodPost "/items"+        reqHeaders+        [json|{"id":0}|]+      `shouldRespondWith`+        [json|[{"id":0}]|]+        { matchStatus  = 201+        , matchHeaders = respHeaders }+    get "/items?id=eq.0"+      `shouldRespondWith`+        [json|[{"id":0}]|]+    deleteItems++  it "does persist put" $ do+    request methodPut "/items?id=eq.0"+        reqHeaders+        [json|{"id":0}|]+      `shouldRespondWith`+        [json|[{"id":0}]|]+        { matchHeaders = respHeaders }+    get "/items?id=eq.0"+      `shouldRespondWith`+        [json|[{"id":0}]|]+    deleteItems++  it "does persist patch" $ do+    postItem+    request methodPatch "/items?id=eq.0"+        reqHeaders+        [json|{"id":-1}|]+      `shouldRespondWith`+        [json|[{"id":-1}]|]+        { matchHeaders = respHeaders }+    get "/items?id=eq.0"+      `shouldRespondWith`+        [json|[]|]+    get "/items?id=eq.-1"+      `shouldRespondWith`+        [json|[{"id":-1}]|]+    deleteItems++  it "does persist delete" $ do+    postItem+    request methodDelete "/items?id=eq.0"+        reqHeaders+        ""+      `shouldRespondWith`+        [json|[{"id":0}]|]+        { matchHeaders = respHeaders }+    get "/items?id=eq.0"+      `shouldRespondWith`+        [json|[]|]++shouldNotPersistMutations reqHeaders respHeaders = do+  it "does not persist post" $ do+    request methodPost "/items"+        reqHeaders+        [json|{"id":0}|]+      `shouldRespondWith`+        [json|[{"id":0}]|]+        { matchStatus  = 201+        , matchHeaders = respHeaders }+    get "/items?id=eq.0"+      `shouldRespondWith`+        [json|[]|]++  it "does not persist put" $ do+    request methodPut "/items?id=eq.0"+        reqHeaders+        [json|{"id":0}|]+      `shouldRespondWith`+        [json|[{"id":0}]|]+        { matchHeaders = respHeaders }+    get "/items?id=eq.0"+      `shouldRespondWith`+        [json|[]|]++  it "does not persist patch" $ do+    request methodPatch "/items?id=eq.1"+        reqHeaders+        [json|{"id":0}|]+      `shouldRespondWith`+        [json|[{"id":0}]|]+        { matchHeaders = respHeaders }+    get "/items?id=eq.0"+      `shouldRespondWith`+        [json|[]|]+    get "items?id=eq.1"+      `shouldRespondWith`+        [json|[{"id":1}]|]++  it "does not persist delete" $ do+    request methodDelete "/items?id=eq.1"+        reqHeaders+        ""+      `shouldRespondWith`+        [json|[{"id":1}]|]+        { matchHeaders = respHeaders }+    get "/items?id=eq.1"+      `shouldRespondWith`+        [json|[{"id":1}]|]++allowed :: SpecWith ((), Application)+allowed = describe "tx-allow-override = true" $ do+  describe "without Prefer tx" $ do+    preferDefault `shouldRespondToReads` withoutPreferenceApplied+    preferDefault `shouldNotPersistMutations` withoutPreferenceApplied+    preferDefault `shouldRaiseExceptions` withoutPreferenceApplied++  describe "Prefer tx=commit" $ do+    preferCommit `shouldRespondToReads` withPreferenceCommitApplied+    preferCommit `shouldPersistMutations` withPreferenceCommitApplied+    -- Exceptions are always without preference applied,+    -- because they return before the end of the transaction.+    preferCommit `shouldRaiseExceptions` withoutPreferenceApplied++  describe "Prefer tx=rollback" $ do+    preferRollback `shouldRespondToReads` withPreferenceRollbackApplied+    preferRollback `shouldNotPersistMutations` withPreferenceRollbackApplied+    -- Exceptions are always without preference applied,+    -- because they return before the end of the transaction.+    preferRollback `shouldRaiseExceptions` withoutPreferenceApplied++disallowed :: SpecWith ((), Application)+disallowed = describe "tx-rollback-all = false, tx-allow-override = false" $ do+  describe "without Prefer tx" $ do+    preferDefault `shouldRespondToReads` withoutPreferenceApplied+    preferDefault `shouldPersistMutations` withoutPreferenceApplied+    preferDefault `shouldRaiseExceptions` withoutPreferenceApplied++  describe "Prefer tx=commit" $ do+    preferCommit `shouldRespondToReads` withoutPreferenceApplied+    preferCommit `shouldPersistMutations` withoutPreferenceApplied+    preferCommit `shouldRaiseExceptions` withoutPreferenceApplied++  describe "Prefer tx=rollback" $ do+    preferRollback `shouldRespondToReads` withoutPreferenceApplied+    preferRollback `shouldPersistMutations` withoutPreferenceApplied+    preferRollback `shouldRaiseExceptions` withoutPreferenceApplied+++forced :: SpecWith ((), Application)+forced = describe "tx-rollback-all = true, tx-allow-override = false" $ do+  describe "without Prefer tx" $ do+    preferDefault `shouldRespondToReads` withoutPreferenceApplied+    preferDefault `shouldNotPersistMutations` withoutPreferenceApplied+    preferDefault `shouldRaiseExceptions` withoutPreferenceApplied++  describe "Prefer tx=commit" $ do+    preferCommit `shouldRespondToReads` withoutPreferenceApplied+    preferCommit `shouldNotPersistMutations` withoutPreferenceApplied+    preferCommit `shouldRaiseExceptions` withoutPreferenceApplied++  describe "Prefer tx=rollback" $ do+    preferRollback `shouldRespondToReads` withoutPreferenceApplied+    preferRollback `shouldNotPersistMutations` withoutPreferenceApplied+    preferRollback `shouldRaiseExceptions` withoutPreferenceApplied+
+ test/spec/Feature/RootSpec.hs view
@@ -0,0 +1,34 @@+module Feature.RootSpec where++import Network.HTTP.Types+import Network.Wai        (Application)++import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude hiding (get)++import SpecHelper++spec :: SpecWith ((), Application)+spec =+  describe "root spec function" $ do+    it "accepts application/openapi+json" $+      request methodGet "/"+        [("Accept","application/openapi+json")] "" `shouldRespondWith`+        [json|{+           "swagger": "2.0",+           "info": {"title": "PostgREST API", "description": "This is a dynamic API generated by PostgREST"}+          }|]+        { matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"] }++    it "accepts application/json" $+      request methodGet "/"+        [("Accept", "application/json")] "" `shouldRespondWith`+        [json| {+            "tableName": "orders_view", "tableSchema": "test",+            "tableDeletable": true, "tableUpdatable": true,+            "tableInsertable": true, "tableDescription": null+          } |]+        { matchHeaders = [matchContentTypeJson] }
+ test/spec/Feature/RpcPreRequestGucsSpec.hs view
@@ -0,0 +1,84 @@+module Feature.RpcPreRequestGucsSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec          hiding (pendingWith)+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude hiding (get, put)++spec :: SpecWith ((), Application)+spec =+  describe "GUC headers on all methods via pre-request" $ do+    it "succeeds setting the headers on POST" $+      post "/items"+          [json|[{"id": 11111}]|]+        `shouldRespondWith` ""+          { matchStatus = 201+          , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]+          }++    it "succeeds setting the headers on GET and HEAD" $ do+      request methodGet "/items?id=eq.1"+          [("User-Agent", "MSIE 6.0")]+          ""+        `shouldRespondWith`+          [json|[{"id": 1}]|]+          { matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"] }++      request methodHead "/items?id=eq.1"+          [("User-Agent", "MSIE 7.0")]+          ""+        `shouldRespondWith`+          ""+          { matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"] }++      request methodHead "/projects"+          [("Accept", "text/csv")]+          ""+        `shouldRespondWith`+          ""+          { matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"] }++    it "succeeds setting the headers on PATCH" $+        patch "/items?id=eq.1"+            [json|[{"id": 11111}]|]+          `shouldRespondWith` ""+            { matchStatus = 204+            , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]+            }++    it "succeeds setting the headers on PUT" $+      put "/items?id=eq.1"+          [json|[{"id": 1}]|]+        `shouldRespondWith` ""+          { matchStatus = 204+          , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]+          }++    it "succeeds setting the headers on DELETE" $+      delete "/items?id=eq.1"+        `shouldRespondWith`+          ""+          { matchStatus = 204+          , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]+          }+    it "can override the Content-Type header" $ do+      request methodHead "/clients?id=eq.1"+          []+          ""+        `shouldRespondWith`+          ""+          { matchStatus = 200+          , matchHeaders = ["Content-Type" <:> "application/custom+json"]+          }+      request methodHead "/rpc/getallprojects"+          []+          ""+        `shouldRespondWith`+          ""+          { matchStatus = 200+          , matchHeaders = ["Content-Type" <:> "application/custom+json"]+          }
+ test/spec/Feature/RpcSpec.hs view
@@ -0,0 +1,1266 @@+module Feature.RpcSpec where++import qualified Data.ByteString.Lazy as BL (empty, readFile)++import Network.Wai      (Application)+import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))++import Network.HTTP.Types+import System.IO.Unsafe    (unsafePerformIO)+import Test.Hspec          hiding (pendingWith)+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON+import Text.Heredoc++import PostgREST.Config.PgVersion (PgVersion, pgVersion100,+                                   pgVersion109, pgVersion110,+                                   pgVersion112, pgVersion114,+                                   pgVersion140)++import Protolude  hiding (get)+import SpecHelper++spec :: PgVersion -> SpecWith ((), Application)+spec actualPgVersion =+  describe "remote procedure call" $ do+    context "a proc that returns a set" $ do+      it "returns paginated results" $ do+        request methodPost "/rpc/getitemrange"+                (rangeHdrs (ByteRangeFromTo 0 0))  [json| { "min": 2, "max": 4 } |]+           `shouldRespondWith` [json| [{"id":3}] |]+            { matchStatus = 200+            , matchHeaders = ["Content-Range" <:> "0-0/*"]+            }+        request methodGet "/rpc/getitemrange?min=2&max=4"+                (rangeHdrs (ByteRangeFromTo 0 0)) ""+           `shouldRespondWith` [json| [{"id":3}] |]+            { matchStatus = 200+            , matchHeaders = ["Content-Range" <:> "0-0/*"]+            }+        request methodHead "/rpc/getitemrange?min=2&max=4"+                (rangeHdrs (ByteRangeFromTo 0 0)) ""+           `shouldRespondWith` ""+            { matchStatus = 200+            , matchHeaders = ["Content-Range" <:> "0-0/*"]+            }++      it "includes total count if requested" $ do+        request methodPost "/rpc/getitemrange"+                (rangeHdrsWithCount (ByteRangeFromTo 0 0))+                [json| { "min": 2, "max": 4 } |]+           `shouldRespondWith` [json| [{"id":3}] |]+            { matchStatus = 206 -- it now knows the response is partial+            , matchHeaders = ["Content-Range" <:> "0-0/2"]+            }+        request methodGet "/rpc/getitemrange?min=2&max=4"+                (rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""+           `shouldRespondWith` [json| [{"id":3}] |]+            { matchStatus = 206+            , matchHeaders = ["Content-Range" <:> "0-0/2"]+            }+        request methodHead "/rpc/getitemrange?min=2&max=4"+                (rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""+           `shouldRespondWith` ""+            { matchStatus = 206+            , matchHeaders = ["Content-Range" <:> "0-0/2"]+            }++      it "returns proper json" $ do+        post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`+          [json| [ {"id": 3}, {"id":4} ] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/rpc/getitemrange?min=2&max=4" `shouldRespondWith`+          [json| [ {"id": 3}, {"id":4} ] |]+          { matchHeaders = [matchContentTypeJson] }++      it "returns CSV" $ do+        request methodPost "/rpc/getitemrange"+                (acceptHdrs "text/csv")+                [json| { "min": 2, "max": 4 } |]+           `shouldRespondWith` "id\n3\n4"+            { matchStatus = 200+            , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]+            }+        request methodGet "/rpc/getitemrange?min=2&max=4"+                (acceptHdrs "text/csv") ""+           `shouldRespondWith` "id\n3\n4"+            { matchStatus = 200+            , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]+            }+        request methodHead "/rpc/getitemrange?min=2&max=4"+                (acceptHdrs "text/csv") ""+           `shouldRespondWith` ""+            { matchStatus = 200+            , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]+            }++    context "unknown function" $ do+      it "returns 404" $+        post "/rpc/fakefunc" [json| {} |] `shouldRespondWith` 404++      it "should fail with 404 on unknown proc name" $+        get "/rpc/fake" `shouldRespondWith` 404++      it "should fail with 404 on unknown proc args" $ do+        get "/rpc/sayhello" `shouldRespondWith` 404+        get "/rpc/sayhello?any_arg=value" `shouldRespondWith` 404++      it "should not ignore unknown args and fail with 404" $+        get "/rpc/add_them?a=1&b=2&smthelse=blabla" `shouldRespondWith`+        [json| {+          "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",+          "message":"Could not find the test.add_them(a, b, smthelse) function in the schema cache" } |]+        { matchStatus  = 404+        , matchHeaders = [matchContentTypeJson]+        }++      it "should fail with 404 when no json arg is found with prefer single object" $+        request methodPost "/rpc/sayhello"+          [("Prefer","params=single-object")]+          [json|{}|]+        `shouldRespondWith`+          [json| {+            "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",+            "message":"Could not find the test.sayhello function with a single json or jsonb parameter in the schema cache" } |]+        { matchStatus  = 404+        , matchHeaders = [matchContentTypeJson]+        }++      it "should fail with 404 for overloaded functions with unknown args" $ do+        get "/rpc/overloaded?wrong_arg=value" `shouldRespondWith`+          [json| {+            "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",+            "message":"Could not find the test.overloaded(wrong_arg) function in the schema cache" } |]+          { matchStatus  = 404+          , matchHeaders = [matchContentTypeJson]+          }+        get "/rpc/overloaded?a=1&b=2&wrong_arg=value" `shouldRespondWith`+          [json| {+            "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",+            "message":"Could not find the test.overloaded(a, b, wrong_arg) function in the schema cache" } |]+          { matchStatus  = 404+          , matchHeaders = [matchContentTypeJson]+          }++    context "ambiguous overloaded functions with same parameters' names but different types" $ do+      it "should fail with 300 Multiple Choices without explicit type casts" $+        get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`+          [json| {+            "hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",+            "message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)"}|]+          { matchStatus  = 300+          , matchHeaders = [matchContentTypeJson]+          }++    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`+          [json|[{"id":1,"name":"Windows 7","client_id":1}]|]+        get "/rpc/getproject?id=1" `shouldRespondWith`+          [json|[{"id":1,"name":"Windows 7","client_id":1}]|]++      it "can filter proc results" $ do+        post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" [json| {} |] `shouldRespondWith`+          [json|[{"id":2},{"id":3},{"id":4}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" `shouldRespondWith`+          [json|[{"id":2},{"id":3},{"id":4}]|]+          { matchHeaders = [matchContentTypeJson] }++      it "can limit proc results" $ do+        post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id&limit=2&offset=1" [json| {} |]+          `shouldRespondWith` [json|[{"id":3},{"id":4}]|]+             { matchStatus = 200+             , matchHeaders = ["Content-Range" <:> "1-2/*"] }+        get "/rpc/getallprojects?id=gt.1&id=lt.5&select=id&limit=2&offset=1"+          `shouldRespondWith` [json|[{"id":3},{"id":4}]|]+             { matchStatus = 200+             , matchHeaders = ["Content-Range" <:> "1-2/*"] }++      it "select works on the first level" $ do+        post "/rpc/getproject?select=id,name" [json| { "id": 1} |] `shouldRespondWith`+          [json|[{"id":1,"name":"Windows 7"}]|]+        get "/rpc/getproject?id=1&select=id,name" `shouldRespondWith`+          [json|[{"id":1,"name":"Windows 7"}]|]++    context "foreign entities embedding" $ do+      it "can embed if related tables are in the exposed schema" $ do+        post "/rpc/getproject?select=id,name,client:clients(id),tasks(id)" [json| { "id": 1} |] `shouldRespondWith`+          [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/rpc/getproject?id=1&select=id,name,client:clients(id),tasks(id)" `shouldRespondWith`+          [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]+          { matchHeaders = [matchContentTypeJson] }++      it "cannot embed if the related table is not in the exposed schema" $ do+        post "/rpc/single_article?select=*,article_stars(*)" [json|{ "id": 1}|]+          `shouldRespondWith` 400+        get "/rpc/single_article?id=1&select=*,article_stars(*)"+          `shouldRespondWith` 400++      it "can embed if the related tables are in a hidden schema but exposed as views" $ do+        post "/rpc/single_article?select=id,articleStars(userId)"+            [json|{ "id": 2}|]+          `shouldRespondWith`+            [json|{"id": 2, "articleStars": [{"userId": 3}]}|]+        get "/rpc/single_article?id=2&select=id,articleStars(userId)"+          `shouldRespondWith`+            [json|{"id": 2, "articleStars": [{"userId": 3}]}|]++      it "can embed an M2M relationship table" $+        get "/rpc/getallusers?select=name,tasks(name)&id=gt.1"+          `shouldRespondWith` [json|[+            {"name":"Michael Scott", "tasks":[{"name":"Design IOS"}, {"name":"Code IOS"}, {"name":"Design OSX"}]},+            {"name":"Dwight Schrute","tasks":[{"name":"Design w7"}, {"name":"Design IOS"}]}+          ]|]+          { matchHeaders = [matchContentTypeJson] }++      it "can embed an M2M relationship table that has a parent relationship table" $+        get "/rpc/getallusers?select=name,tasks(name,project:projects(name))&id=gt.1"+          `shouldRespondWith` [json|[+            {"name":"Michael Scott","tasks":[+              {"name":"Design IOS","project":{"name":"IOS"}},+              {"name":"Code IOS","project":{"name":"IOS"}},+              {"name":"Design OSX","project":{"name":"OSX"}}+            ]},+            {"name":"Dwight Schrute","tasks":[+              {"name":"Design w7","project":{"name":"Windows 7"}},+              {"name":"Design IOS","project":{"name":"IOS"}}+            ]}+          ]|]+          { matchHeaders = [matchContentTypeJson] }++      when (actualPgVersion >= pgVersion110) $+        it "can embed if rpc returns domain of table type" $ do+          post "/rpc/getproject_domain?select=id,name,client:clients(id),tasks(id)"+              [json| { "id": 1} |]+            `shouldRespondWith`+              [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]+          get "/rpc/getproject_domain?id=1&select=id,name,client:clients(id),tasks(id)"+            `shouldRespondWith`+              [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]++    context "a proc that returns an empty rowset" $+      it "returns empty json array" $ do+        post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`+          [json| [] |]+          { matchHeaders = [matchContentTypeJson] }+        get "/rpc/test_empty_rowset" `shouldRespondWith`+          [json| [] |]+          { matchHeaders = [matchContentTypeJson] }++    context "proc return types" $ do+      context "returns text" $ do+        it "returns proper json" $+          post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`+            [json|"Hello, world"|]+            { matchHeaders = [matchContentTypeJson] }++        it "can handle unicode" $+          post "/rpc/sayhello" [json| { "name": "¥" } |] `shouldRespondWith`+            [json|"Hello, ¥"|]+            { matchHeaders = [matchContentTypeJson] }++      it "returns array" $+        post "/rpc/ret_array" [json|{}|] `shouldRespondWith`+          [json|[1, 2, 3]|]+          { matchHeaders = [matchContentTypeJson] }++      it "returns setof integers" $+        post "/rpc/ret_setof_integers"+            [json|{}|]+          `shouldRespondWith`+            [json|[1,2,3]|]++      it "returns enum value" $+        post "/rpc/ret_enum" [json|{ "val": "foo" }|] `shouldRespondWith`+          [json|"foo"|]+          { matchHeaders = [matchContentTypeJson] }++      it "returns domain value" $+        post "/rpc/ret_domain" [json|{ "val": "8" }|] `shouldRespondWith`+          [json|8|]+          { matchHeaders = [matchContentTypeJson] }++      it "returns range" $+        post "/rpc/ret_range" [json|{ "low": 10, "up": 20 }|] `shouldRespondWith`+          [json|"[10,20)"|]+          { matchHeaders = [matchContentTypeJson] }++      it "returns row of scalars" $+        post "/rpc/ret_scalars" [json|{}|] `shouldRespondWith`+          [json|[{"a":"scalars", "b":"foo", "c":1, "d":"[10,20)"}]|]+          { matchHeaders = [matchContentTypeJson] }++      it "returns composite type in exposed schema" $+        post "/rpc/ret_point_2d"+            [json|{}|]+          `shouldRespondWith`+            [json|{"x": 10, "y": 5}|]++      it "cannot return composite type in hidden schema" $+        post "/rpc/ret_point_3d" [json|{}|] `shouldRespondWith` 401++      when (actualPgVersion >= pgVersion110) $+        it "returns domain of composite type" $+          post "/rpc/ret_composite_domain"+              [json|{}|]+            `shouldRespondWith`+              [json|{"x": 10, "y": 5}|]++      it "returns single row from table" $+        post "/rpc/single_article?select=id"+            [json|{"id": 2}|]+          `shouldRespondWith`+            [json|{"id": 2}|]++      it "returns null for void" $+        post "/rpc/ret_void"+            [json|{}|]+          `shouldRespondWith`+            "null"+            { matchHeaders = [matchContentTypeJson] }++      it "returns null for an integer with null value" $+        post "/rpc/ret_null"+            [json|{}|]+          `shouldRespondWith`+            "null"+            { matchHeaders = [matchContentTypeJson] }++      context "different types when overloaded" $ do+        it "returns composite type" $+          post "/rpc/ret_point_overloaded"+              [json|{"x": 1, "y": 2}|]+            `shouldRespondWith`+              [json|{"x": 1, "y": 2}|]++        it "returns json scalar with prefer single object" $+          request methodPost "/rpc/ret_point_overloaded" [("Prefer","params=single-object")]+            [json|{"x": 1, "y": 2}|]+            `shouldRespondWith`+            [json|{"x": 1, "y": 2}|]+            { matchHeaders = [matchContentTypeJson] }++    context "proc argument types" $ do+      -- different syntax for array needed for pg<10+      when (actualPgVersion < pgVersion100) $+        it "accepts a variety of arguments (Postgres < 10)" $+          post "/rpc/varied_arguments"+              [json| {+                "double": 3.1,+                "varchar": "hello",+                "boolean": true,+                "date": "20190101",+                "money": 0,+                "enum": "foo",+                "arr": "{a,b,c}",+                "integer": 43,+                "json": {"some key": "some value"},+                "jsonb": {"another key": [1, 2, "3"]}+              } |]+            `shouldRespondWith`+              [json| {+                "double": 3.1,+                "varchar": "hello",+                "boolean": true,+                "date": "2019-01-01",+                "money": "$0.00",+                "enum": "foo",+                "arr": ["a", "b", "c"],+                "integer": 43,+                "json": {"some key": "some value"},+                "jsonb": {"another key": [1, 2, "3"]}+              } |]+              { matchHeaders = [matchContentTypeJson] }++      when (actualPgVersion >= pgVersion100) $+        it "accepts a variety of arguments (Postgres >= 10)" $+          post "/rpc/varied_arguments"+              [json| {+                "double": 3.1,+                "varchar": "hello",+                "boolean": true,+                "date": "20190101",+                "money": 0,+                "enum": "foo",+                "arr": ["a", "b", "c"],+                "integer": 43,+                "json": {"some key": "some value"},+                "jsonb": {"another key": [1, 2, "3"]}+              } |]+            `shouldRespondWith`+              [json| {+                "double": 3.1,+                "varchar": "hello",+                "boolean": true,+                "date": "2019-01-01",+                "money": "$0.00",+                "enum": "foo",+                "arr": ["a", "b", "c"],+                "integer": 43,+                "json": {"some key": "some value"},+                "jsonb": {"another key": [1, 2, "3"]}+              } |]+              { matchHeaders = [matchContentTypeJson] }++      it "accepts a variety of arguments with GET" $+        -- without JSON / JSONB here, because passing those via query string is useless - they just become a "json string" all the time+        get "/rpc/varied_arguments?double=3.1&varchar=hello&boolean=true&date=20190101&money=0&enum=foo&arr=%7Ba,b,c%7D&integer=43"+          `shouldRespondWith`+              [json| {+                "double": 3.1,+                "varchar": "hello",+                "boolean": true,+                "date": "2019-01-01",+                "money": "$0.00",+                "enum": "foo",+                "arr": ["a", "b", "c"],+                "integer": 43,+                "json": {},+                "jsonb": {}+              } |]+            { matchHeaders = [matchContentTypeJson] }++      it "accepts a variety of arguments from an html form" $+        request methodPost "/rpc/varied_arguments"+            [("Content-Type", "application/x-www-form-urlencoded")]+            "double=3.1&varchar=hello&boolean=true&date=20190101&money=0&enum=foo&arr=%7Ba,b,c%7D&integer=43"+          `shouldRespondWith`+              [json| {+                "double": 3.1,+                "varchar": "hello",+                "boolean": true,+                "date": "2019-01-01",+                "money": "$0.00",+                "enum": "foo",+                "arr": ["a", "b", "c"],+                "integer": 43,+                "json": {},+                "jsonb": {}+              } |]+            { matchHeaders = [matchContentTypeJson] }++      it "parses embedded JSON arguments as JSON" $+        post "/rpc/json_argument"+            [json| { "arg": { "key": 3 } } |]+          `shouldRespondWith`+            [json|"object"|]+            { matchHeaders = [matchContentTypeJson] }++      when (actualPgVersion < pgVersion100) $+        it "parses quoted JSON arguments as JSON (Postgres < 10)" $+          post "/rpc/json_argument"+              [json| { "arg": "{ \"key\": 3 }" } |]+            `shouldRespondWith`+              [json|"object"|]+              { matchHeaders = [matchContentTypeJson] }++      when ((actualPgVersion >= pgVersion109 && actualPgVersion < pgVersion110)+            || actualPgVersion >= pgVersion114) $+        it "parses quoted JSON arguments as JSON string (from Postgres 10.9, 11.4)" $+          post "/rpc/json_argument"+              [json| { "arg": "{ \"key\": 3 }" } |]+            `shouldRespondWith`+              [json|"string"|]+              { matchHeaders = [matchContentTypeJson] }++    context "improper input" $ do+      it "rejects unknown content type even if payload is good" $ do+        request methodPost "/rpc/sayhello"+          (acceptHdrs "audio/mpeg3") [json| { "name": "world" } |]+            `shouldRespondWith` 415+        request methodGet "/rpc/sayhello?name=world"+          (acceptHdrs "audio/mpeg3") ""+            `shouldRespondWith` 415+      it "rejects malformed json payload" $ do+        p <- request methodPost "/rpc/sayhello"+          (acceptHdrs "application/json") "sdfsdf"+        liftIO $ do+          simpleStatus p `shouldBe` badRequest400+          isErrorFormat (simpleBody p) `shouldBe` True+      it "treats simple plpgsql raise as invalid input" $ do+        p <- post "/rpc/problem" "{}"+        liftIO $ do+          simpleStatus p `shouldBe` badRequest400+          isErrorFormat (simpleBody p) `shouldBe` True+      it "treats plpgsql assert as internal server error" $ do+        p <- post "/rpc/assert" "{}"+        liftIO $ do+          simpleStatus p `shouldBe` internalServerError500+          isErrorFormat (simpleBody p) `shouldBe` True++    context "unsupported verbs" $ do+      it "DELETE fails" $+        request methodDelete "/rpc/sayhello" [] ""+          `shouldRespondWith`+          [json|{"message":"Bad Request"}|]+          { matchStatus  = 405+          , matchHeaders = [matchContentTypeJson]+          }+      it "PATCH fails" $+        request methodPatch "/rpc/sayhello" [] ""+          `shouldRespondWith` 405+      it "OPTIONS fails" $+        -- TODO: should return info about the function+        request methodOptions "/rpc/sayhello" [] ""+          `shouldRespondWith` 405++    it "executes the proc exactly once per request" $ do+      -- callcounter is persistent even with rollback, because it uses a sequence+      -- reset counter first to make test repeatable+      request methodPost "/rpc/reset_sequence"+          [("Prefer", "tx=commit")]+          [json|{"name": "callcounter_count", "value": 1}|]+        `shouldRespondWith`+          [json|""|]++      -- now the test+      post "/rpc/callcounter"+          [json|{}|]+        `shouldRespondWith`+          [json|1|]++      post "/rpc/callcounter"+          [json|{}|]+        `shouldRespondWith`+          [json|2|]++    context "a proc that receives no parameters" $ do+      it "interprets empty string as empty json object on a post request" $+        post "/rpc/noparamsproc" BL.empty `shouldRespondWith`+          [json| "Return value of no parameters procedure." |]+          { matchHeaders = [matchContentTypeJson] }+      it "interprets empty string as a function with no args on a get request" $+        get "/rpc/noparamsproc" `shouldRespondWith`+          [json| "Return value of no parameters procedure." |]+          { matchHeaders = [matchContentTypeJson] }++    it "returns proper output when having the same return col name as the proc name" $ do+      post "/rpc/test" [json|{}|] `shouldRespondWith`+        [json|[{"test":"hello","value":1}]|] { matchHeaders = [matchContentTypeJson] }+      get "/rpc/test" `shouldRespondWith`+        [json|[{"test":"hello","value":1}]|] { matchHeaders = [matchContentTypeJson] }++    context "procs with OUT/INOUT params" $ do+      it "returns an object result when there is a single OUT param" $ do+        get "/rpc/single_out_param?num=5"+          `shouldRespondWith`+            [json|{"num_plus_one":6}|]++        get "/rpc/single_json_out_param?a=1&b=two"+          `shouldRespondWith`+            [json|{"my_json": {"a": 1, "b": "two"}}|]++      it "returns an object result when there is a single INOUT param" $+        get "/rpc/single_inout_param?num=2"+          `shouldRespondWith`+            [json|{"num":3}|]++      it "returns an object result when there are many OUT params" $+        get "/rpc/many_out_params"+          `shouldRespondWith`+            [json|{"my_json":{"a": 1, "b": "two"},"num":3,"str":"four"}|]++      it "returns an object result when there are many INOUT params" $+        get "/rpc/many_inout_params?num=1&str=two&b=false"+          `shouldRespondWith`+            [json|{"num":1,"str":"two","b":false}|]++    context "procs with TABLE return" $ do+      it "returns an object result when there is a single-column TABLE return type" $+        get "/rpc/single_column_table_return"+          `shouldRespondWith`+            [json|[{"a": "A"}]|]++      it "returns an object result when there is a multi-column TABLE return type" $+        get "/rpc/multi_column_table_return"+          `shouldRespondWith`+            [json|[{"a": "A", "b": "B"}]|]++    context "procs with VARIADIC params" $ do+      when (actualPgVersion < pgVersion100) $+        it "works with POST (Postgres < 10)" $+          post "/rpc/variadic_param"+              [json| { "v": "{hi,hello,there}" } |]+            `shouldRespondWith`+              [json|["hi", "hello", "there"]|]++      when (actualPgVersion >= pgVersion100) $ do+        it "works with POST (Postgres >= 10)" $+          post "/rpc/variadic_param"+              [json| { "v": ["hi", "hello", "there"] } |]+            `shouldRespondWith`+              [json|["hi", "hello", "there"]|]++        context "works with GET and repeated params" $ do+          it "n=0 (through DEFAULT)" $+            get "/rpc/variadic_param"+              `shouldRespondWith`+                [json|[]|]++          it "n=1" $+            get "/rpc/variadic_param?v=hi"+              `shouldRespondWith`+                [json|["hi"]|]++          it "n>1" $+            get "/rpc/variadic_param?v=hi&v=there"+              `shouldRespondWith`+                [json|["hi", "there"]|]++        context "works with POST and repeated params from html form" $ do+          it "n=0 (through DEFAULT)" $+            request methodPost "/rpc/variadic_param"+                [("Content-Type", "application/x-www-form-urlencoded")]+                ""+              `shouldRespondWith`+                [json|[]|]++          it "n=1" $+            request methodPost "/rpc/variadic_param"+                [("Content-Type", "application/x-www-form-urlencoded")]+                "v=hi"+              `shouldRespondWith`+                [json|["hi"]|]++          it "n>1" $+            request methodPost "/rpc/variadic_param"+                [("Content-Type", "application/x-www-form-urlencoded")]+                "v=hi&v=there"+              `shouldRespondWith`+                [json|["hi", "there"]|]++    it "returns last value for repeated params without VARIADIC" $+      get "/rpc/sayhello?name=ignored&name=world"+        `shouldRespondWith`+          [json|"Hello, world"|]++    when (actualPgVersion >= pgVersion100) $+      it "returns last value for repeated non-variadic params in function with other VARIADIC arguments" $+        get "/rpc/sayhello_variadic?name=ignored&name=world&v=unused"+          `shouldRespondWith`+            [json|"Hello, world"|]++    it "can handle procs with args that have a DEFAULT value" $ do+      get "/rpc/many_inout_params?num=1&str=two"+        `shouldRespondWith`+          [json| {"num":1,"str":"two","b":true}|]+      get "/rpc/three_defaults?b=4"+        `shouldRespondWith`+          [json|8|]++    it "can map a RAISE error code and message to a http status" $+      get "/rpc/raise_pt402"+        `shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded" }|]+        { matchStatus  = 402+        , matchHeaders = [matchContentTypeJson]+        }++    it "defaults to status 500 if RAISE code is PT not followed by a number" $+      get "/rpc/raise_bad_pt"+        `shouldRespondWith`+        [json|{"hint": null, "details": null}|]+        { matchStatus  = 500+        , matchHeaders = [ matchContentTypeJson ]+        }++    context "expects a single json object" $ do+      it "does not expand posted json into parameters" $+        request methodPost "/rpc/singlejsonparam"+          [("prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith`+          [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |]+          { matchHeaders = [matchContentTypeJson] }++      it "accepts parameters from an html form" $+        request methodPost "/rpc/singlejsonparam"+          [("Prefer","params=single-object"),("Content-Type", "application/x-www-form-urlencoded")]+          ("integer=7&double=2.71828&varchar=forms+are+fun&" <>+           "boolean=false&date=1900-01-01&money=$3.99&enum=foo") `shouldRespondWith`+          [json| { "integer": "7", "double": "2.71828", "varchar" : "forms are fun"+                 , "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |]+                 { matchHeaders = [matchContentTypeJson] }++      it "works with GET" $+        request methodGet "/rpc/singlejsonparam?p1=1&p2=text" [("Prefer","params=single-object")] ""+          `shouldRespondWith` [json|{ "p1": "1", "p2": "text"}|]+          { matchHeaders = [matchContentTypeJson] }++    context "should work with an overloaded function" $ do+      it "overloaded()" $+        get "/rpc/overloaded"+          `shouldRespondWith`+            [json|[1,2,3]|]++      it "overloaded(json) single-object" $+        request methodPost "/rpc/overloaded"+            [("Prefer","params=single-object")]+            [json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]+          `shouldRespondWith`+            [json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]++      it "overloaded(int, int)" $+        get "/rpc/overloaded?a=1&b=2" `shouldRespondWith` [str|3|]++      it "overloaded(text, text, text)" $+        get "/rpc/overloaded?a=1&b=2&c=3" `shouldRespondWith` [json|"123"|]++      it "overloaded_html_form()" $+        request methodPost "/rpc/overloaded_html_form"+            [("Content-Type", "application/x-www-form-urlencoded")]+            ""+          `shouldRespondWith`+            [json|[1,2,3]|]++      it "overloaded_html_form(json) single-object" $+        request methodPost "/rpc/overloaded_html_form"+            [("Content-Type", "application/x-www-form-urlencoded"), ("Prefer","params=single-object")]+            "a=1&b=2&c=3"+          `shouldRespondWith`+            [json|{"a": "1", "b": "2", "c": "3"}|]++      it "overloaded_html_form(int, int)" $+        request methodPost "/rpc/overloaded_html_form"+            [("Content-Type", "application/x-www-form-urlencoded")]+            "a=1&b=2"+          `shouldRespondWith`+            [str|3|]++      it "overloaded_html_form(text, text, text)" $+        request methodPost "/rpc/overloaded_html_form"+            [("Content-Type", "application/x-www-form-urlencoded")]+            "a=1&b=2&c=3"+          `shouldRespondWith`+            [json|"123"|]++      -- https://github.com/PostgREST/postgrest/issues/1672+      context "embedding overloaded functions with the same signature except for the last param with a default value" $ do+        it "overloaded_default(text default)" $ do+          request methodPost "/rpc/overloaded_default?select=id,name,users(name)"+              [("Content-Type", "application/json")]+              [json|{}|]+            `shouldRespondWith`+              [json|[{"id": 2, "name": "Code w7", "users": [{"name": "Angela Martin"}]}] |]++        it "overloaded_default(int)" $+          request methodPost "/rpc/overloaded_default"+              [("Content-Type", "application/json")]+              [json|{"must_param":1}|]+            `shouldRespondWith`+              [json|{"val":1}|]++        it "overloaded_default(int, text default)" $ do+          request methodPost "/rpc/overloaded_default?select=id,name,users(name)"+              [("Content-Type", "application/json")]+              [json|{"a":4}|]+            `shouldRespondWith`+              [json|[{"id": 5, "name": "Design IOS", "users": [{"name": "Michael Scott"}, {"name": "Dwight Schrute"}]}] |]++        it "overloaded_default(int, int)" $+          request methodPost "/rpc/overloaded_default"+              [("Content-Type", "application/json")]+              [json|{"a":2,"must_param":4}|]+            `shouldRespondWith`+              [json|{"a":2,"val":4}|]++    context "only for POST rpc" $ do+      it "gives a parse filter error if GET style proc args are specified" $+        post "/rpc/sayhello?name=John" [json|{name: "John"}|] `shouldRespondWith` 400++      it "ignores json keys not included in ?columns" $+        post "/rpc/sayhello?columns=name"+          [json|{"name": "John", "smth": "here", "other": "stuff", "fake_id": 13}|]+          `shouldRespondWith`+          [json|"Hello, John"|]+          { matchHeaders = [matchContentTypeJson] }++      it "only takes the first object in case of array of objects payload" $+        post "/rpc/add_them"+          [json|[+            {"a": 1, "b": 2},+            {"a": 4, "b": 6},+            {"a": 100, "b": 200} ]|]+          `shouldRespondWith` "3"+          { matchHeaders = [matchContentTypeJson] }++    context "bulk RPC with params=multiple-objects" $ do+      it "works with a scalar function an returns a json array" $+        request methodPost "/rpc/add_them" [("Prefer", "params=multiple-objects")]+          [json|[+            {"a": 1, "b": 2},+            {"a": 4, "b": 6},+            {"a": 100, "b": 200} ]|]+          `shouldRespondWith`+          [json|+            [3, 10, 300]+          |] { matchHeaders = [matchContentTypeJson] }++      it "works with a scalar function an returns a json array when posting CSV" $+        request methodPost "/rpc/add_them" [("Content-Type", "text/csv"), ("Prefer", "params=multiple-objects")]+          "a,b\n1,2\n4,6\n100,200"+          `shouldRespondWith`+          [json|+            [3, 10, 300]+          |]+          { matchStatus  = 200+          , matchHeaders = [matchContentTypeJson]+          }++      it "works with a non-scalar result" $+        request methodPost "/rpc/get_projects_below?select=id,name" [("Prefer", "params=multiple-objects")]+          [json|[+            {"id": 1},+            {"id": 5} ]|]+          `shouldRespondWith`+          [json|+            [{"id":1,"name":"Windows 7"},+             {"id":2,"name":"Windows 10"},+             {"id":3,"name":"IOS"},+             {"id":4,"name":"OSX"}]+          |] { matchHeaders = [matchContentTypeJson] }++    context "HTTP request env vars" $ do+      it "custom header is set" $+        request methodPost "/rpc/get_guc_value"+                  [("Custom-Header", "test")]+            (+            if actualPgVersion >= pgVersion140 then+              [json| { "prefix": "request.headers", "name": "custom-header" } |]+            else+              [json| { "name": "request.header.custom-header" } |]+            )+            `shouldRespondWith`+            [json|"test"|]+            { matchStatus  = 200+            , matchHeaders = [ matchContentTypeJson ]+            }+      it "standard header is set" $+        request methodPost "/rpc/get_guc_value"+                  [("Origin", "http://example.com")]+            (+            if actualPgVersion >= pgVersion140 then+              [json| { "prefix": "request.headers", "name": "origin" } |]+            else+              [json| { "name": "request.header.origin" } |]+            )+            `shouldRespondWith`+            [json|"http://example.com"|]+            { matchStatus  = 200+            , matchHeaders = [ matchContentTypeJson ]+            }+      it "current role is available as GUC claim" $+        request methodPost "/rpc/get_guc_value" []+            (+            if actualPgVersion >= pgVersion140 then+              [json| { "prefix": "request.jwt.claims", "name": "role" } |]+            else+              [json| { "name": "request.jwt.claim.role" } |]+            )+            `shouldRespondWith`+            [json|"postgrest_test_anonymous"|]+            { matchStatus  = 200+            , matchHeaders = [ matchContentTypeJson ]+            }+      it "single cookie ends up as claims" $+        request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]+          (+          if actualPgVersion >= pgVersion140 then+            [json| {"prefix": "request.cookies", "name":"acookie"} |]+          else+            [json| {"name":"request.cookie.acookie"} |]+          )+            `shouldRespondWith`+            [json|"cookievalue"|]+            { matchStatus = 200+            , matchHeaders = []+            }+      it "multiple cookies ends up as claims" $+        request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]+          (+          if actualPgVersion >= pgVersion140 then+            [json| {"prefix": "request.cookies", "name":"secondcookie"} |]+          else+            [json| {"name":"request.cookie.secondcookie"} |]+          )+            `shouldRespondWith`+            [json|"anothervalue"|]+            { matchStatus = 200+            , matchHeaders = []+            }+      it "app settings available" $+        request methodPost "/rpc/get_guc_value" []+          [json| { "name": "app.settings.app_host" } |]+            `shouldRespondWith`+            [json|"localhost"|]+            { matchStatus  = 200+            , matchHeaders = [ matchContentTypeJson ]+            }+      it "gets the Authorization value" $+        request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]+          (+          if actualPgVersion >= pgVersion140 then+            [json| {"prefix": "request.headers", "name":"authorization"} |]+          else+            [json| {"name":"request.header.authorization"} |]+          )+            `shouldRespondWith`+            [json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]+            { matchStatus = 200+            , matchHeaders = []+            }+      it "gets the http method" $+        request methodPost "/rpc/get_guc_value" []+          [json| {"name":"request.method"} |]+            `shouldRespondWith`+            [json|"POST"|]+            { matchStatus = 200+            , matchHeaders = []+            }+      it "gets the http path" $+        request methodPost "/rpc/get_guc_value" []+          [json| {"name":"request.path"} |]+            `shouldRespondWith`+            [json|"/rpc/get_guc_value"|]+            { matchStatus = 200+            , matchHeaders = []+            }++    context "binary output" $ do+      context "Proc that returns scalar" $ do+        it "can query without selecting column" $+          request methodPost "/rpc/ret_base64_bin" (acceptHdrs "application/octet-stream") ""+            `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"+            { matchStatus = 200+            , matchHeaders = ["Content-Type" <:> "application/octet-stream"]+            }++        it "can get raw output with Accept: text/plain" $+          request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""+            `shouldRespondWith` "Welcome to PostgREST"+            { matchStatus = 200+            , matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]+            }++      context "Proc that returns set of scalars" $+        it "can query without selecting column" $+          request methodGet "/rpc/welcome_twice"+              (acceptHdrs "text/plain")+              ""+            `shouldRespondWith`+              "Welcome to PostgRESTWelcome to PostgREST"+              { matchStatus = 200+              , matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]+              }++      context "Proc that returns rows" $ do+        it "can query if a single column is selected" $+          request methodPost "/rpc/ret_rows_with_base64_bin?select=img" (acceptHdrs "application/octet-stream") ""+            `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="+            { matchStatus = 200+            , matchHeaders = ["Content-Type" <:> "application/octet-stream"]+            }++        it "fails if a single column is not selected" $+          request methodPost "/rpc/ret_rows_with_base64_bin"+              (acceptHdrs "application/octet-stream") ""+            `shouldRespondWith`+              [json| {"message":"application/octet-stream requested but more than one column was selected"} |]+              { matchStatus = 406 }++    context "only for GET rpc" $ do+      it "should fail on mutating procs" $ do+        get "/rpc/callcounter" `shouldRespondWith` 405+        get "/rpc/setprojects?id_l=1&id_h=5&name=FreeBSD" `shouldRespondWith` 405++      it "should filter a proc that has arg name = filter name" $+        get "/rpc/get_projects_below?id=5&id=gt.2&select=id" `shouldRespondWith`+          [json|[{ "id": 3 }, { "id": 4 }]|]+          { matchHeaders = [matchContentTypeJson] }++      it "should work with filters that have the not operator" $ do+        get "/rpc/get_projects_below?id=5&id=not.gt.2&select=id" `shouldRespondWith`+          [json|[{ "id": 1 }, { "id": 2 }]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/rpc/get_projects_below?id=5&id=not.in.(1,3)&select=id" `shouldRespondWith`+          [json|[{ "id": 2 }, { "id": 4 }]|]+          { matchHeaders = [matchContentTypeJson] }++      it "should work with filters that use the plain with language fts operator" $ do+        get "/rpc/get_tsearch?text_search_vector=fts(english).impossible" `shouldRespondWith`+          [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/rpc/get_tsearch?text_search_vector=plfts.impossible" `shouldRespondWith`+          [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/rpc/get_tsearch?text_search_vector=not.fts(english).fun%7Crat" `shouldRespondWith`+          [json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|]+          { matchHeaders = [matchContentTypeJson] }+        when (actualPgVersion >= pgVersion112) $+            get "/rpc/get_tsearch?text_search_vector=wfts.impossible" `shouldRespondWith`+                [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]+                { matchHeaders = [matchContentTypeJson] }++      it "should work with the phraseto_tsquery function" $+        get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`+          [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]+          { 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] }++    context "GUC headers on function calls" $ do+      it "succeeds setting the headers" $ do+        get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id"+          `shouldRespondWith` [json|[{"id": 2}]|]+          {matchHeaders = [+              matchContentTypeJson,+              "X-Test"   <:> "key1=val1; someValue; key2=val2",+              "X-Test-2" <:> "key1=val1"]}+        get "/rpc/get_int_and_guc_headers?num=1"+          `shouldRespondWith` [json|1|]+          {matchHeaders = [+              matchContentTypeJson,+              "X-Test"   <:> "key1=val1; someValue; key2=val2",+              "X-Test-2" <:> "key1=val1"]}+        post "/rpc/get_int_and_guc_headers" [json|{"num": 1}|]+          `shouldRespondWith` [json|1|]+          {matchHeaders = [+              matchContentTypeJson,+              "X-Test"   <:> "key1=val1; someValue; key2=val2",+              "X-Test-2" <:> "key1=val1"]}++      it "fails when setting headers with wrong json structure" $ do+        get "/rpc/bad_guc_headers_1"+          `shouldRespondWith`+          [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]+          { matchStatus  = 500+          , matchHeaders = [ matchContentTypeJson ]+          }+        get "/rpc/bad_guc_headers_2"+          `shouldRespondWith`+          [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]+          { matchStatus  = 500+          , matchHeaders = [ matchContentTypeJson ]+          }+        get "/rpc/bad_guc_headers_3"+          `shouldRespondWith`+          [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]+          { matchStatus  = 500+          , matchHeaders = [ matchContentTypeJson ]+          }+        post "/rpc/bad_guc_headers_1" [json|{}|]+          `shouldRespondWith`+          [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]+          { matchStatus  = 500+          , matchHeaders = [ matchContentTypeJson ]+          }++      it "can set the same http header twice" $+        get "/rpc/set_cookie_twice"+          `shouldRespondWith`+            "null"+            { matchHeaders = [ matchContentTypeJson+                             , "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/"+                             , "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly" ]}++      it "can override the Location header on a trigger" $+        post "/stuff"+            [json|[{"id": 2, "name": "stuff 2"}]|]+          `shouldRespondWith`+            ""+            { matchStatus = 201+            , matchHeaders = ["Location" <:> "/stuff?id=eq.2&overriden=true"]+            }++      -- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535+      -- it was reported that blank headers ` : ` where added and that cause proxies to fail the requests.+      -- These tests are to ensure no blank headers are added.+      context "Blank headers bug" $ do+        it "shouldn't add blank headers on POST" $ do+          r <- request methodPost "/loc_test" [] [json|{"id": "1", "c": "c1"}|]+          liftIO $ do+            let respHeaders = simpleHeaders r+            respHeaders `shouldSatisfy` noBlankHeader++        it "shouldn't add blank headers on PATCH" $ do+          r <- request methodPatch "/loc_test?id=eq.1" [] [json|{"c": "c2"}|]+          liftIO $ do+            let respHeaders = simpleHeaders r+            respHeaders `shouldSatisfy` noBlankHeader++        it "shouldn't add blank headers on GET" $ do+          r <- request methodGet "/loc_test" [] ""+          liftIO $ do+            let respHeaders = simpleHeaders r+            respHeaders `shouldSatisfy` noBlankHeader++        it "shouldn't add blank headers on DELETE" $ do+          r <- request methodDelete "/loc_test?id=eq.1" [] ""+          liftIO $ do+            let respHeaders = simpleHeaders r+            respHeaders `shouldSatisfy` noBlankHeader++      context "GUC status override" $ do+        it "can override the status on RPC" $+          get "/rpc/send_body_status_403"+            `shouldRespondWith`+            [json|{"message" : "invalid user or password"}|]+            { matchStatus  = 403+            , matchHeaders = [ matchContentTypeJson ]+            }++        it "can override the status through trigger" $+          patch "/stuff?id=eq.1"+              [json|[{"name": "updated stuff 1"}]|]+            `shouldRespondWith`+              205++        it "fails when setting invalid status guc" $+          get "/rpc/send_bad_status"+            `shouldRespondWith`+            [json|{"message":"response.status guc must be a valid status code"}|]+            { matchStatus  = 500+            , matchHeaders = [ matchContentTypeJson ]+            }++      context "single unnamed param" $ do+        it "can insert json directly" $+          post "/rpc/unnamed_json_param"+              [json|{"A": 1, "B": 2, "C": 3}|]+            `shouldRespondWith`+              [json|{"A": 1, "B": 2, "C": 3}|]++        it "can insert text directly" $+          request methodPost "/rpc/unnamed_text_param"+            [("Content-Type", "text/plain"), ("Accept", "text/plain")]+            [str|unnamed text arg|]+            `shouldRespondWith`+            [str|unnamed text arg|]++        it "can insert bytea directly" $ do+          let file = unsafePerformIO $ BL.readFile "test/spec/fixtures/image.png"+          r <- request methodPost "/rpc/unnamed_bytea_param"+            [("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]+            file+          liftIO $ do+            let respBody = simpleBody r+            respBody `shouldBe` file++        it "will err when no function with single unnamed json parameter exists and application/json is specified" $+          request methodPost "/rpc/unnamed_int_param" [("Content-Type", "application/json")]+              [json|{"x": 1, "y": 2}|]+            `shouldRespondWith`+              [json|{+                "hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",+                "message": "Could not find the test.unnamed_int_param(x, y) function or the test.unnamed_int_param function with a single unnamed json or jsonb parameter in the schema cache"+              }|]+              { matchStatus  = 404+              , matchHeaders = [ matchContentTypeJson ]+              }++        it "will err when no function with single unnamed text parameter exists and text/plain is specified" $+          request methodPost "/rpc/unnamed_int_param"+              [("Content-Type", "text/plain")]+              [str|a simple text|]+            `shouldRespondWith`+              [json|{+                "hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",+                "message": "Could not find the test.unnamed_int_param function with a single unnamed text parameter in the schema cache"+              }|]+              { matchStatus  = 404+              , matchHeaders = [ matchContentTypeJson ]+              }++        it "will err when no function with single unnamed bytea parameter exists and application/octet-stream is specified" $+          let file = unsafePerformIO $ BL.readFile "test/spec/fixtures/image.png" in+          request methodPost "/rpc/unnamed_int_param"+              [("Content-Type", "application/octet-stream")]+              file+          `shouldRespondWith`+            [json|{+              "hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",+              "message": "Could not find the test.unnamed_int_param function with a single unnamed bytea parameter in the schema cache"+            }|]+            { matchStatus  = 404+            , matchHeaders = [ matchContentTypeJson ]+            }++        it "should be able to resolve when a single unnamed json parameter exists and other overloaded functions are found" $ do+          request methodPost "/rpc/overloaded_unnamed_param" [("Content-Type", "application/json")]+              [json|{}|]+            `shouldRespondWith`+              [json| 1 |]+              { matchStatus  = 200+              , matchHeaders = [matchContentTypeJson]+              }+          request methodPost "/rpc/overloaded_unnamed_param" [("Content-Type", "application/json")]+              [json|{"x": 1, "y": 2}|]+            `shouldRespondWith`+              [json| 3 |]+              { matchStatus  = 200+              , matchHeaders = [matchContentTypeJson]+              }++        it "should be able to fallback to the single unnamed parameter function when other overloaded functions are not found" $ do+          request methodPost "/rpc/overloaded_unnamed_param"+              [("Content-Type", "application/json")]+              [json|{"A": 1, "B": 2, "C": 3}|]+            `shouldRespondWith`+              [json|{"A": 1, "B": 2, "C": 3}|]+          request methodPost "/rpc/overloaded_unnamed_param"+              [("Content-Type", "text/plain"), ("Accept", "text/plain")]+              [str|unnamed text arg|]+            `shouldRespondWith`+              [str|unnamed text arg|]+          let file = unsafePerformIO $ BL.readFile "test/spec/fixtures/image.png"+          r <- request methodPost "/rpc/overloaded_unnamed_param"+            [("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]+            file+          liftIO $ do+            let respBody = simpleBody r+            respBody `shouldBe` file++        it "should call the function with no parameters and not fallback to the single unnamed parameter function when using GET with Content-Type headers" $ do+          request methodGet "/rpc/overloaded_unnamed_param" [("Content-Type", "text/plain")] ""+            `shouldRespondWith`+              [json| 1|]+              { matchStatus  = 200 }+          request methodGet "/rpc/overloaded_unnamed_param" [("Content-Type", "application/octet-stream")] ""+            `shouldRespondWith`+              [json| 1|]+              { matchStatus  = 200 }++        it "should fail to fallback to any single unnamed parameter function when using an unsupported Content-Type header" $ do+          request methodPost "/rpc/overloaded_unnamed_param"+              [("Content-Type", "text/csv")]+              "a,b\n1,2\n4,6\n100,200"+            `shouldRespondWith`+              [json| {+                "hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",+                "message":"Could not find the test.overloaded_unnamed_param(a, b) function in the schema cache"}|]+              { matchStatus  = 404+              , matchHeaders = [matchContentTypeJson]+              }++        it "should fail with multiple choices when two fallback functions with single unnamed json and jsonb parameters exist" $ do+          request methodPost "/rpc/overloaded_unnamed_json_jsonb_param" [("Content-Type", "application/json")]+              [json|{"A": 1, "B": 2, "C": 3}|]+            `shouldRespondWith`+              [json| {+                "hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",+                "message":"Could not choose the best candidate function between: test.overloaded_unnamed_json_jsonb_param( => json), test.overloaded_unnamed_json_jsonb_param( => jsonb)"}|]+              { matchStatus  = 300+              , matchHeaders = [matchContentTypeJson]+              }
+ test/spec/Feature/SingularSpec.hs view
@@ -0,0 +1,324 @@+module Feature.SingularSpec where++import Network.Wai      (Application)+import Network.Wai.Test (SResponse (..))++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude  hiding (get)+import SpecHelper+++spec :: SpecWith ((), Application)+spec =+  describe "Requesting singular json object" $ do+    let singular = ("Accept", "application/vnd.pgrst.object+json")++    context "with GET request" $ do+      it "fails for zero rows" $+        request methodGet  "/items?id=gt.0&id=lt.0" [singular] ""+          `shouldRespondWith` 406++      it "will select an existing object" $ do+        request methodGet "/items?id=eq.5" [singular] ""+          `shouldRespondWith`+            [json|{"id":5}|]+            { matchHeaders = [matchContentTypeSingular] }+        -- also test without the +json suffix+        request methodGet "/items?id=eq.5"+            [("Accept", "application/vnd.pgrst.object")] ""+          `shouldRespondWith`+            [json|{"id":5}|]+            { matchHeaders = [matchContentTypeSingular] }++      it "can combine multiple prefer values" $+        request methodGet "/items?id=eq.5" [singular, ("Prefer","count=none")] ""+          `shouldRespondWith`+            [json|{"id":5}|]+            { matchHeaders = [matchContentTypeSingular] }++      it "can shape plurality singular object routes" $+        request methodGet "/projects_view?id=eq.1&select=id,name,clients(*),tasks(id,name)" [singular] ""+          `shouldRespondWith`+            [json|{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}|]+            { matchHeaders = [matchContentTypeSingular] }++    context "when updating rows" $ do+      it "works for one row with return=rep" $ do+        request methodPatch "/addresses?id=eq.1"+            [("Prefer", "return=representation"), singular]+            [json| { address: "B Street" } |]+          `shouldRespondWith`+            [json|{"id":1,"address":"B Street"}|]+            { matchHeaders = [matchContentTypeSingular] }++      it "works for one row with return=minimal" $+        request methodPatch "/addresses?id=eq.1"+            [("Prefer", "return=minimal"), singular]+            [json| { address: "C Street" } |]+          `shouldRespondWith`+            ""+            { matchStatus  = 204 }++      it "raises an error for multiple rows" $ do+        request methodPatch "/addresses"+            [("Prefer", "tx=commit"), singular]+            [json| { address: "zzz" } |]+          `shouldRespondWith`+            [json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+            { matchStatus  = 406+            , matchHeaders = [ matchContentTypeSingular+                             , "Preference-Applied" <:> "tx=commit" ]+            }++        -- the rows should not be updated, either+        get "/addresses?id=eq.1"+          `shouldRespondWith`+            [json|[{"id":1,"address":"address 1"}]|]++      it "raises an error for multiple rows with return=rep" $ do+        request methodPatch "/addresses"+            [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]+            [json| { address: "zzz" } |]+          `shouldRespondWith`+            [json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+            { matchStatus  = 406+            , matchHeaders = [ matchContentTypeSingular+                             , "Preference-Applied" <:> "tx=commit" ]+            }++        -- the rows should not be updated, either+        get "/addresses?id=eq.1"+          `shouldRespondWith`+            [json|[{"id":1,"address":"address 1"}]|]++      it "raises an error for zero rows" $+        request methodPatch "/items?id=gt.0&id=lt.0"+                [singular] [json|{"id":1}|]+          `shouldRespondWith`+                  [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+                  { matchStatus  = 406+                  , matchHeaders = [matchContentTypeSingular]+                  }++      it "raises an error for zero rows with return=rep" $+        request methodPatch "/items?id=gt.0&id=lt.0"+                [("Prefer", "return=representation"), singular] [json|{"id":1}|]+          `shouldRespondWith`+                  [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+                  { matchStatus  = 406+                  , matchHeaders = [matchContentTypeSingular]+                  }++    context "when creating rows" $ do+      it "works for one row with return=rep" $ do+        request methodPost "/addresses"+            [("Prefer", "return=representation"), singular]+            [json| [ { id: 102, address: "xxx" } ] |]+          `shouldRespondWith`+            [json|{"id":102,"address":"xxx"}|]+            { matchStatus  = 201+            , matchHeaders = [matchContentTypeSingular]+            }++      it "works for one row with return=minimal" $ do+        request methodPost "/addresses"+            [("Prefer", "return=minimal"), singular]+            [json| [ { id: 103, address: "xxx" } ] |]+          `shouldRespondWith` ""+            { matchStatus  = 201+            , matchHeaders = ["Content-Range" <:> "*/*"]+            }++      it "raises an error when attempting to create multiple entities" $ do+        request methodPost "/addresses"+            [("Prefer", "tx=commit"), singular]+            [json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]+          `shouldRespondWith`+            [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+            { matchStatus  = 406+            , matchHeaders = [ matchContentTypeSingular+                             , "Preference-Applied" <:> "tx=commit" ]+            }++        -- the rows should not exist, either+        get "/addresses?id=eq.200"+          `shouldRespondWith`+            "[]"++      it "raises an error when attempting to create multiple entities with return=rep" $ do+        request methodPost "/addresses"+            [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]+            [json| [ { id: 202, address: "xxx" }, { id: 203, address: "yyy" } ] |]+          `shouldRespondWith`+            [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+            { matchStatus  = 406+            , matchHeaders = [ matchContentTypeSingular+                             , "Preference-Applied" <:> "tx=commit" ]+            }++        -- the rows should not exist, either+        get "/addresses?id=eq.202"+          `shouldRespondWith`+            "[]"++      it "raises an error regardless of return=minimal" $ do+        request methodPost "/addresses"+            [("Prefer", "tx=commit"), ("Prefer", "return=minimal"), singular]+            [json| [ { id: 204, address: "xxx" }, { id: 205, address: "yyy" } ] |]+          `shouldRespondWith`+            [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+            { matchStatus  = 406+            , matchHeaders = [ matchContentTypeSingular+                             , "Preference-Applied" <:> "tx=commit" ]+            }++        -- the rows should not exist, either+        get "/addresses?id=eq.204"+          `shouldRespondWith`+            "[]"++      it "raises an error when creating zero entities" $+        request methodPost "/addresses"+                [singular]+                [json| [ ] |]+          `shouldRespondWith`+                  [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+                  { matchStatus  = 406+                  , matchHeaders = [matchContentTypeSingular]+                  }++      it "raises an error when creating zero entities with return=rep" $+        request methodPost "/addresses"+                [("Prefer", "return=representation"), singular]+                [json| [ ] |]+          `shouldRespondWith`+                  [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+                  { matchStatus  = 406+                  , matchHeaders = [matchContentTypeSingular]+                  }++    context "when deleting rows" $ do+      it "works for one row with return=rep" $ do+        p <- request methodDelete+          "/items?id=eq.11"+          [("Prefer", "return=representation"), singular] ""+        liftIO $ simpleBody p `shouldBe` [json|{"id":11}|]++      it "works for one row with return=minimal" $ do+        p <- request methodDelete+          "/items?id=eq.12"+          [("Prefer", "return=minimal"), singular] ""+        liftIO $ simpleBody p `shouldBe` ""++      it "raises an error when attempting to delete multiple entities" $ do+        request methodDelete "/items?id=gt.0&id=lt.6"+            [("Prefer", "tx=commit"), singular]+            ""+          `shouldRespondWith`+            [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+            { matchStatus  = 406+            , matchHeaders = [ matchContentTypeSingular+                             , "Preference-Applied" <:> "tx=commit" ]+            }++        -- the rows should still exist+        get "/items?id=gt.0&id=lt.6&order=id"+          `shouldRespondWith`+            [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}] |]+            { matchStatus  = 200+            , matchHeaders = ["Content-Range" <:> "0-4/*"]+            }++      it "raises an error when attempting to delete multiple entities with return=rep" $ do+        request methodDelete "/items?id=gt.5&id=lt.11"+            [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular] ""+          `shouldRespondWith`+            [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+            { matchStatus  = 406+            , matchHeaders = [ matchContentTypeSingular+                             , "Preference-Applied" <:> "tx=commit" ]+            }++        -- the rows should still exist+        get "/items?id=gt.5&id=lt.11"+          `shouldRespondWith` [json| [{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |]+            { matchStatus  = 200+            , matchHeaders = ["Content-Range" <:> "0-4/*"]+            }++      it "raises an error when deleting zero entities" $+        request methodDelete "/items?id=lt.0"+                [singular] ""+          `shouldRespondWith`+                [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+                { matchStatus  = 406+                , matchHeaders = [matchContentTypeSingular]+                }++      it "raises an error when deleting zero entities with return=rep" $+        request methodDelete "/items?id=lt.0"+                [("Prefer", "return=representation"), singular] ""+          `shouldRespondWith`+                [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+                { matchStatus  = 406+                , matchHeaders = [matchContentTypeSingular]+                }++    context "when calling a stored proc" $ do+      it "fails for zero rows" $+        request methodPost "/rpc/getproject"+                [singular] [json|{ "id": 9999999}|]+          `shouldRespondWith`+                [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+                { matchStatus  = 406+                , matchHeaders = [matchContentTypeSingular]+                }++      -- this one may be controversial, should vnd.pgrst.object include+      -- the likes of 2 and "hello?"+      it "succeeds for scalar result" $+        request methodPost "/rpc/sayhello"+          [singular] [json|{ "name": "world"}|]+          `shouldRespondWith` 200++      it "returns a single object for json proc" $+        request methodPost "/rpc/getproject"+            [singular] [json|{ "id": 1}|]+          `shouldRespondWith`+            [json|{"id":1,"name":"Windows 7","client_id":1}|]+            { matchHeaders = [matchContentTypeSingular] }++      it "fails for multiple rows" $+        request methodPost "/rpc/getallprojects"+                [singular] "{}"+          `shouldRespondWith`+                [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+                { matchStatus  = 406+                , matchHeaders = [matchContentTypeSingular]+                }++      it "fails for multiple rows with rolled back changes" $ do+        post "/rpc/getproject?select=id,name"+            [json| {"id": 1} |]+          `shouldRespondWith`+            [json|[{"id":1,"name":"Windows 7"}]|]++        request methodPost "/rpc/setprojects"+            [("Prefer", "tx=commit"), singular]+            [json| {"id_l": 1, "id_h": 2, "name": "changed"} |]+          `shouldRespondWith`+            [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]+            { matchStatus  = 406+            , matchHeaders = [ matchContentTypeSingular+                             , "Preference-Applied" <:> "tx=commit" ]+            }++        -- should rollback function+        post "/rpc/getproject?select=id,name"+            [json| {"id": 1} |]+          `shouldRespondWith`+            [json|[{"id":1,"name":"Windows 7"}]|]
+ test/spec/Feature/UnicodeSpec.hs view
@@ -0,0 +1,34 @@+module Feature.UnicodeSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude hiding (get)++spec :: SpecWith ((), Application)+spec =+  describe "Reading and writing to unicode schema and table names" $+    it "Can read and write values" $ do+      get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"+        `shouldRespondWith` "[]"++      request methodPost "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"+          [("Prefer", "tx=commit"), ("Prefer", "return=representation")]+          [json| { "هویت": 1 } |]+        `shouldRespondWith`+          [json| [{ "هویت": 1 }] |]+          { matchStatus = 201 }++      get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"+        `shouldRespondWith`+          [json| [{ "هویت": 1 }] |]++      request methodDelete "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"+          [("Prefer", "tx=commit")]+          ""+        `shouldRespondWith`+          204
+ test/spec/Feature/UpdateSpec.hs view
@@ -0,0 +1,349 @@+module Feature.UpdateSpec where++import Network.Wai (Application)+import Test.Hspec  hiding (pendingWith)++import Network.HTTP.Types+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import Protolude  hiding (get)+import SpecHelper++spec :: SpecWith ((), Application)+spec = do+  describe "Patching record" $ do+    context "to unknown uri" $+      it "indicates no table found by returning 404" $+        request methodPatch "/fake" []+          [json| { "real": false } |]+            `shouldRespondWith` 404++    context "on an empty table" $+      it "indicates no records found to update by returning 404" $+        request methodPatch "/empty_table" []+          [json| { "extra":20 } |]+          `shouldRespondWith` ""+          { matchStatus  = 404,+            matchHeaders = []+          }++    context "with invalid json payload" $+      it "fails with 400 and error" $+        request methodPatch "/simple_pk" [] "}{ x = 2"+          `shouldRespondWith`+          [json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]+          { matchStatus  = 400,+            matchHeaders = [matchContentTypeJson]+          }++    context "with no payload" $+      it "fails with 400 and error" $+        request methodPatch "/items" [] ""+          `shouldRespondWith`+          [json|{"message":"Error in $: not enough input"}|]+          { matchStatus  = 400,+            matchHeaders = [matchContentTypeJson]+          }++    context "in a nonempty table" $ do+      it "can update a single item" $ do+        patch "/items?id=eq.2"+            [json| { "id":42 } |]+          `shouldRespondWith`+            ""+            { matchStatus  = 204+            , matchHeaders = ["Content-Range" <:> "0-0/*"]+            }++      it "returns empty array when no rows updated and return=rep" $+        request methodPatch "/items?id=eq.999999"+          [("Prefer", "return=representation")] [json| { "id":999999 } |]+          `shouldRespondWith` "[]"+          {+            matchStatus  = 404,+            matchHeaders = []+          }++      it "gives a 404 when no rows updated" $+        request methodPatch "/items?id=eq.99999999" []+          [json| { "id": 42 } |]+            `shouldRespondWith` 404++      it "returns updated object as array when return=rep" $+        request methodPatch "/items?id=eq.2"+          [("Prefer", "return=representation")] [json| { "id":2 } |]+          `shouldRespondWith` [json|[{"id":2}]|]+          { matchStatus  = 200,+            matchHeaders = ["Content-Range" <:> "0-0/*"]+          }++      it "can update multiple items" $ do+        get "/no_pk?select=a&b=eq.1"+          `shouldRespondWith`+            [json|[]|]++        request methodPatch "/no_pk?b=eq.0"+            [("Prefer", "tx=commit")]+            [json| { b: "1" } |]+          `shouldRespondWith`+            ""+            { matchStatus  = 204+            , matchHeaders = ["Content-Range" <:> "0-1/*"+                             , "Preference-Applied" <:> "tx=commit" ]+            }++        -- check it really got updated+        get "/no_pk?select=a&b=eq.1"+          `shouldRespondWith`+            [json|[ { a: "1" }, { a: "2" } ]|]++        -- put value back for other tests+        request methodPatch "/no_pk?b=eq.1"+            [("Prefer", "tx=commit")]+            [json| { b: "0" } |]+          `shouldRespondWith`+            204++      it "can set a column to NULL" $ do+        request methodPatch "/no_pk?a=eq.1"+            [("Prefer", "return=representation")]+            [json| { b: null } |]+          `shouldRespondWith`+            [json| [{ a: "1", b: null }] |]++      context "filtering by a computed column" $ do+        it "is successful" $+          request methodPatch+            "/items?is_first=eq.true"+            [("Prefer", "return=representation")]+            [json| { id: 100 } |]+            `shouldRespondWith` [json| [{ id: 100 }] |]+            { matchStatus  = 200,+              matchHeaders = [matchContentTypeJson, "Content-Range" <:> "0-0/*"]+            }++        it "indicates no records updated by returning 404" $+          request methodPatch+            "/items?always_true=eq.false"+            [("Prefer", "return=representation")]+            [json| { id: 100 } |]+            `shouldRespondWith` "[]"+            { matchStatus  = 404,+              matchHeaders = []+            }++      context "with representation requested" $ do+        it "can provide a representation" $ do+          _ <- post "/items"+            [json| { id: 1 } |]+          request methodPatch+            "/items?id=eq.1"+            [("Prefer", "return=representation")]+            [json| { id: 99 } |]+            `shouldRespondWith` [json| [{id:99}] |]+            { matchHeaders = [matchContentTypeJson] }+          -- put value back for other tests+          void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]++        it "can return computed columns" $+          request methodPatch+            "/items?id=eq.1&select=id,always_true"+            [("Prefer", "return=representation")]+            [json| { id: 1 } |]+            `shouldRespondWith` [json| [{ id: 1, always_true: true }] |]+            { matchHeaders = [matchContentTypeJson] }++        it "can select overloaded computed columns" $ do+          request methodPatch+            "/items?id=eq.1&select=id,computed_overload"+            [("Prefer", "return=representation")]+            [json| { id: 1 } |]+            `shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]+            { matchHeaders = [matchContentTypeJson] }+          request methodPatch+            "/items2?id=eq.1&select=id,computed_overload"+            [("Prefer", "return=representation")]+            [json| { id: 1 } |]+            `shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]+            { matchHeaders = [matchContentTypeJson] }++      it "ignores ?select= when return not set or return=minimal" $ do+        request methodPatch "/items?id=eq.1&select=id" [] [json| { id:1 } |]+          `shouldRespondWith` ""+          {+            matchStatus  = 204,+            matchHeaders = ["Content-Range" <:> "0-0/*"]+          }+        request methodPatch "/items?id=eq.1&select=id" [("Prefer", "return=minimal")] [json| { id:1 } |]+          `shouldRespondWith` ""+          {+            matchStatus  = 204,+            matchHeaders = ["Content-Range" <:> "0-0/*"]+          }++      context "when patching with an empty body" $ do+        it "makes no updates and returns 204 without return= and without ?select=" $ do+          request methodPatch "/items" [] [json| {} |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++          request methodPatch "/items" [] [json| [] |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++          request methodPatch "/items" [] [json| [{}] |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "makes no updates and returns 204 without return= and with ?select=" $ do+          request methodPatch "/items?select=id" [] [json| {} |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++          request methodPatch "/items?select=id" [] [json| [] |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++          request methodPatch "/items?select=id" [] [json| [{}] |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "makes no updates and returns 200 with return=rep and without ?select=" $+          request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |]+            `shouldRespondWith` "[]"+            {+              matchStatus  = 200,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "makes no updates and returns 200 with return=rep and with ?select=" $+          request methodPatch "/items?select=id" [("Prefer", "return=representation")] [json| {} |]+            `shouldRespondWith` "[]"+            {+              matchStatus  = 200,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "makes no updates and returns 200 with return=rep and with ?select= for overloaded computed columns" $+          request methodPatch "/items?select=id,computed_overload" [("Prefer", "return=representation")] [json| {} |]+            `shouldRespondWith` "[]"+            {+              matchStatus  = 200,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++    context "with unicode values" $+      it "succeeds and returns values intact" $ do+        request methodPatch "/no_pk?a=eq.1"+            [("Prefer", "return=representation")]+            [json| { "a":"圍棋", "b":"¥" } |]+          `shouldRespondWith`+            [json|[ { "a":"圍棋", "b":"¥" } ]|]++    context "PATCH with ?columns parameter" $ do+      it "ignores json keys not included in ?columns" $ do+        request methodPatch "/articles?id=eq.1&columns=body"+            [("Prefer", "return=representation")]+            [json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |]+          `shouldRespondWith`+            [json|[{"id": 1, "body": "Some real content", "owner": "postgrest_test_anonymous"}]|]++      it "ignores json keys and gives 404 if no record updated" $+        request methodPatch "/articles?id=eq.2001&columns=body" [("Prefer", "return=representation")]+          [json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith` 404++  context "tables with self reference foreign keys" $ do+    it "embeds children after update" $+      request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)"+              [("Prefer", "return=representation")]+        [json|{"name": "tardis-patched"}|]+        `shouldRespondWith`+        [json|+          [ { "id": 0, "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]+        |]+        { matchStatus  = 200,+          matchHeaders = [matchContentTypeJson]+        }++    it "embeds parent, children and grandchildren after update" $+      request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)"+              [("Prefer", "return=representation")]+        [json|{"name": "tardis-patched-2"}|]+        `shouldRespondWith`+        [json| [+          {+            "id": 0,+            "name": "tardis-patched-2",+            "parent_content": { "name": "wat" },+            "web_content": [+                { "name": "fezz", "web_content": [ { "name": "wut" } ] },+                { "name": "foo",  "web_content": [] },+                { "name": "bar",  "web_content": [] }+            ]+          }+        ] |]+        { matchStatus  = 200,+          matchHeaders = [matchContentTypeJson]+        }++    it "embeds children after update without explicitly including the id in the ?select" $+      request methodPatch "/web_content?id=eq.0&select=name,web_content(name)"+              [("Prefer", "return=representation")]+        [json|{"name": "tardis-patched"}|]+        `shouldRespondWith`+        [json|+          [ { "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]+        |]+        { matchStatus  = 200,+          matchHeaders = [matchContentTypeJson]+        }++    it "embeds an M2M relationship plus parent after update" $+      request methodPatch "/users?id=eq.1&select=name,tasks(name,project:projects(name))"+              [("Prefer", "return=representation")]+        [json|{"name": "Kevin Malone"}|]+        `shouldRespondWith`+        [json|[+          {+            "name": "Kevin Malone",+            "tasks": [+                { "name": "Design w7", "project": { "name": "Windows 7" } },+                { "name": "Code w7", "project": { "name": "Windows 7" } },+                { "name": "Design w10", "project": { "name": "Windows 10" } },+                { "name": "Code w10", "project": { "name": "Windows 10" } }+            ]+          }+        ]|]+        { matchStatus  = 200,+          matchHeaders = [matchContentTypeJson]+        }++  context "table with limited privileges" $ do+    it "succeeds updating row and gives a 204 when using return=minimal" $+      request methodPatch "/app_users?id=eq.1" [("Prefer", "return=minimal")]+        [json| { "password": "passxyz" } |]+          `shouldRespondWith` 204++    it "can update without return=minimal and no explicit select" $+      request methodPatch "/app_users?id=eq.1" []+        [json| { "password": "passabc" } |]+          `shouldRespondWith` 204
+ test/spec/Feature/UpsertSpec.hs view
@@ -0,0 +1,427 @@+module Feature.UpsertSpec where++import Network.Wai (Application)++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Test.Hspec.Wai.JSON++import PostgREST.Config.PgVersion (PgVersion, pgVersion110)++import Protolude  hiding (get, put)+import SpecHelper++spec :: PgVersion -> SpecWith ((), Application)+spec actualPgVersion =+  describe "UPSERT" $ do+    context "with POST" $ do+      context "when Prefer: resolution=merge-duplicates is specified" $ do+        it "INSERTs and UPDATEs rows on pk conflict" $+          request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]+            [json| [+              { "name": "Javascript", "rank": 6 },+              { "name": "Java", "rank": 2 },+              { "name": "C", "rank": 1 }+            ]|] `shouldRespondWith` [json| [+              { "name": "Javascript", "rank": 6 },+              { "name": "Java", "rank": 2 },+              { "name": "C", "rank": 1 }+            ]|]+            { matchStatus = 201+            , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]+            }++        it "INSERTs and UPDATEs row on composite pk conflict" $+          request methodPost "/employees" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]+            [json| [+              { "first_name": "Frances M.", "last_name": "Roe", "salary": "30000" },+              { "first_name": "Peter S.", "last_name": "Yang", "salary": 42000 }+            ]|] `shouldRespondWith` [json| [+              { "first_name": "Frances M.", "last_name": "Roe", "salary": "$30,000.00", "company": "One-Up Realty", "occupation": "Author" },+              { "first_name": "Peter S.", "last_name": "Yang", "salary": "$42,000.00", "company": null, "occupation": null }+            ]|]+            { matchStatus = 201+            , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]+            }++        when (actualPgVersion >= pgVersion110) $+          it "INSERTs and UPDATEs rows on composite pk conflict for partitioned tables" $+            request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]+              [json| [+                { "name": "Murcielago", "year": 2001, "car_brand_name": null},+                { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" }+              ]|] `shouldRespondWith` [json| [+                { "name": "Murcielago", "year": 2001, "car_brand_name": null},+                { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" }+              ]|]+              { matchStatus = 201+              , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]+              }++        it "succeeds when the payload has no elements" $+          request methodPost "/articles" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]+            [json|[]|] `shouldRespondWith`+            [json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }++        it "INSERTs and UPDATEs rows on single unique key conflict" $+          request methodPost "/single_unique?on_conflict=unique_key" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]+            [json| [+              { "unique_key": 1, "value": "B" },+              { "unique_key": 2, "value": "C" }+            ]|] `shouldRespondWith` [json| [+              { "unique_key": 1, "value": "B" },+              { "unique_key": 2, "value": "C" }+            ]|]+            { matchStatus = 201+            , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]+            }++        it "INSERTs and UPDATEs rows on compound unique keys conflict" $+          request methodPost "/compound_unique?on_conflict=key1,key2" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]+            [json| [+              { "key1": 1, "key2": 1, "value": "B" },+              { "key1": 1, "key2": 2, "value": "C" }+            ]|] `shouldRespondWith` [json| [+              { "key1": 1, "key2": 1, "value": "B" },+              { "key1": 1, "key2": 2, "value": "C" }+            ]|]+            { matchStatus = 201+            , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]+            }++      context "when Prefer: resolution=ignore-duplicates is specified" $ do+        it "INSERTs and ignores rows on pk conflict" $+          request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]+            [json|[+              { "name": "PHP", "rank": 9 },+              { "name": "Python", "rank": 10 }+            ]|] `shouldRespondWith` [json|[+              { "name": "PHP", "rank": 9 }+            ]|]+            { matchStatus = 201+            , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]+            }++        it "INSERTs and ignores rows on composite pk conflict" $+          request methodPost "/employees" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]+            [json|[+              { "first_name": "Daniel B.", "last_name": "Lyon", "salary": "72000", "company": null, "occupation": null },+              { "first_name": "Sara M.", "last_name": "Torpey", "salary": 60000, "company": "Burstein-Applebee", "occupation": "Soil scientist" }+            ]|] `shouldRespondWith` [json|[+              { "first_name": "Sara M.", "last_name": "Torpey", "salary": "$60,000.00", "company": "Burstein-Applebee", "occupation": "Soil scientist" }+            ]|]+            { matchStatus = 201+            , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]+            }++        when (actualPgVersion >= pgVersion110) $+          it "INSERTs and ignores rows on composite pk conflict for partitioned tables" $+            request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]+              [json| [+                { "name": "Murcielago", "year": 2001, "car_brand_name": "Ferrari" },+                { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" }+              ]|] `shouldRespondWith` [json| [+                { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" }+              ]|]+              { matchStatus = 201+              , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]+              }++        it "INSERTs and ignores rows on single unique key conflict" $+          request methodPost "/single_unique?on_conflict=unique_key"+              [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]+              [json| [+                { "unique_key": 1, "value": "B" },+                { "unique_key": 2, "value": "C" },+                { "unique_key": 3, "value": "D" }+              ]|]+            `shouldRespondWith`+              [json| [+                { "unique_key": 2, "value": "C" },+                { "unique_key": 3, "value": "D" }+              ]|]+              { matchStatus = 201+              , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates"]+              }++        it "INSERTs and UPDATEs rows on compound unique keys conflict" $+          request methodPost "/compound_unique?on_conflict=key1,key2"+              [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]+              [json| [+                { "key1": 1, "key2": 1, "value": "B" },+                { "key1": 1, "key2": 2, "value": "C" },+                { "key1": 1, "key2": 3, "value": "D" }+              ]|]+            `shouldRespondWith`+              [json| [+                { "key1": 1, "key2": 2, "value": "C" },+                { "key1": 1, "key2": 3, "value": "D" }+              ]|]+              { matchStatus = 201+              , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates"]+              }++      it "succeeds if the table has only PK cols and no other cols" $ do+        request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]+          [json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]+          `shouldRespondWith`+          [json|[ { "id": 3} ]|]+          { matchStatus = 201 ,+            matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates",+            matchContentTypeJson] }++        request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]+          [json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]+          `shouldRespondWith`+          [json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]+          { matchStatus = 201 ,+            matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates",+            matchContentTypeJson] }++      it "succeeds and ignores the Prefer: resolution header(no Preference-Applied present) if the table has no PK" $+        request methodPost "/no_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]+          [json|[ { "a": "1", "b": "0" } ]|]+          `shouldRespondWith`+          [json|[ { "a": "1", "b": "0" } ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }++      it "succeeds if not a single resource is created" $ do+        request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]+          [json|[ { "name": "Java", "rank": 1 } ]|] `shouldRespondWith`+          [json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }+        request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]+          [json|[ { "name": "Java", "rank": 1 }, { "name": "C", "rank": 2 } ]|] `shouldRespondWith`+          [json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }++    context "with PUT" $ do+      context "Restrictions" $ do+        it "fails if Range is specified" $+          request methodPut "/tiobe_pls?name=eq.Javascript" [("Range", "0-5")]+            [json| [ { "name": "Javascript", "rank": 1 } ]|]+            `shouldRespondWith`+            [json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]+            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }++        it "fails if limit is specified" $+          put "/tiobe_pls?name=eq.Javascript&limit=1"+            [json| [ { "name": "Javascript", "rank": 1 } ]|]+            `shouldRespondWith`+            [json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]+            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }++        it "fails if offset is specified" $+          put "/tiobe_pls?name=eq.Javascript&offset=1"+            [json| [ { "name": "Javascript", "rank": 1 } ]|]+            `shouldRespondWith`+            [json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]+            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }++        it "rejects every other filter than pk cols eq's" $ do+          put "/tiobe_pls?rank=eq.19"+            [json| [ { "name": "Go", "rank": 19 } ]|]+            `shouldRespondWith`+            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]+            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }++          put "/tiobe_pls?id=not.eq.Java"+            [json| [ { "name": "Go", "rank": 19 } ]|]+            `shouldRespondWith`+            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]+            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }+          put "/tiobe_pls?id=in.(Go)"+            [json| [ { "name": "Go", "rank": 19 } ]|]+            `shouldRespondWith`+            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]+            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }+          put "/tiobe_pls?and=(id.eq.Go)"+            [json| [ { "name": "Go", "rank": 19 } ]|]+            `shouldRespondWith`+            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]+            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }++        it "fails if not all composite key cols are specified as eq filters" $ do+          put "/employees?first_name=eq.Susan"+            [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]+            `shouldRespondWith`+            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]+            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }+          put "/employees?last_name=eq.Heidt"+            [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]+            `shouldRespondWith`+            [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]+            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }++      it "fails if the uri primary key doesn't match the payload primary key" $ do+        put "/tiobe_pls?name=eq.MATLAB" [json| [ { "name": "Perl", "rank": 17 } ]|]+          `shouldRespondWith`+          [json|{"message":"Payload values do not match URL in primary key column(s)"}|]+          { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }+        put "/employees?first_name=eq.Wendy&last_name=eq.Anderson"+          [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]+          `shouldRespondWith`+          [json|{"message":"Payload values do not match URL in primary key column(s)"}|]+          { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }++      it "fails if the table has no PK" $+        put "/no_pk?a=eq.one&b=eq.two" [json| [ { "a": "one", "b": "two" } ]|]+          `shouldRespondWith`+          [json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]+          { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }++      context "Inserting row" $ do+        it "succeeds on table with single pk col" $ do+          -- assert that the next request will indeed be an insert+          get "/tiobe_pls?name=eq.Go"+            `shouldRespondWith`+              [json|[]|]++          request methodPut "/tiobe_pls?name=eq.Go"+              [("Prefer", "return=representation")]+              [json| [ { "name": "Go", "rank": 19 } ]|]+            `shouldRespondWith`+              [json| [ { "name": "Go", "rank": 19 } ]|]++        it "succeeds on table with composite pk" $ do+          -- assert that the next request will indeed be an insert+          get "/employees?first_name=eq.Susan&last_name=eq.Heidt"+            `shouldRespondWith`+              [json|[]|]++          request methodPut "/employees?first_name=eq.Susan&last_name=eq.Heidt"+              [("Prefer", "return=representation")]+              [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]+            `shouldRespondWith`+              [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]++        when (actualPgVersion >= pgVersion110) $+          it "succeeds on a partitioned table with composite pk" $ do+            -- assert that the next request will indeed be an insert+            get "/car_models?name=eq.Supra&year=eq.2021"+              `shouldRespondWith`+                [json|[]|]++            request methodPut "/car_models?name=eq.Supra&year=eq.2021"+                [("Prefer", "return=representation")]+                [json| [ { "name": "Supra", "year": 2021 } ]|]+              `shouldRespondWith`+                [json| [ { "name": "Supra", "year": 2021, "car_brand_name": null } ]|]++        it "succeeds if the table has only PK cols and no other cols" $ do+          -- assert that the next request will indeed be an insert+          get "/only_pk?id=eq.10"+            `shouldRespondWith`+              [json|[]|]++          request methodPut "/only_pk?id=eq.10"+              [("Prefer", "return=representation")]+              [json|[ { "id": 10 } ]|]+            `shouldRespondWith`+              [json|[ { "id": 10 } ]|]++      context "Updating row" $ do+        it "succeeds on table with single pk col" $ do+          -- assert that the next request will indeed be an update+          get "/tiobe_pls?name=eq.Java"+            `shouldRespondWith`+              [json|[ { "name": "Java", "rank": 1 } ]|]++          request methodPut "/tiobe_pls?name=eq.Java"+              [("Prefer", "return=representation")]+              [json| [ { "name": "Java", "rank": 13 } ]|]+            `shouldRespondWith`+              [json| [ { "name": "Java", "rank": 13 } ]|]++        -- TODO: move this to SingularSpec?+        it "succeeds if the payload has more than one row, but it only puts the first element" $ do+          -- assert that the next request will indeed be an update+          get "/tiobe_pls?name=eq.Java"+            `shouldRespondWith`+              [json|[ { "name": "Java", "rank": 1 } ]|]++          request methodPut "/tiobe_pls?name=eq.Java"+              [("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]+              [json| [ { "name": "Java", "rank": 19 }, { "name": "Swift", "rank": 12 } ] |]+            `shouldRespondWith`+              [json|{ "name": "Java", "rank": 19 }|]+              { matchHeaders = [matchContentTypeSingular] }++        it "succeeds on table with composite pk" $ do+          -- assert that the next request will indeed be an update+          get "/employees?first_name=eq.Frances M.&last_name=eq.Roe"+            `shouldRespondWith`+              [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$24,000.00", "company": "One-Up Realty", "occupation": "Author" } ]|]++          request methodPut "/employees?first_name=eq.Frances M.&last_name=eq.Roe"+              [("Prefer", "return=representation")]+              [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "60000", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]+            `shouldRespondWith`+              [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]++        when (actualPgVersion >= pgVersion110) $+          it "succeeds on a partitioned table with composite pk" $ do+            -- assert that the next request will indeed be an update+            get "/car_models?name=eq.DeLorean&year=eq.1981"+              `shouldRespondWith`+                [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": "DMC" } ]|]++            request methodPut "/car_models?name=eq.DeLorean&year=eq.1981"+                [("Prefer", "return=representation")]+                [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|]+              `shouldRespondWith`+                [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|]++        it "succeeds if the table has only PK cols and no other cols" $ do+          -- assert that the next request will indeed be an update+          get "/only_pk?id=eq.1"+            `shouldRespondWith`+              [json|[ { "id": 1 } ]|]++          request methodPut "/only_pk?id=eq.1"+              [("Prefer", "return=representation")]+              [json|[ { "id": 1 } ]|]+            `shouldRespondWith`+              [json|[ { "id": 1 } ]|]++      -- TODO: move this to SingularSpec?+      it "works with return=representation and vnd.pgrst.object+json" $+        request methodPut "/tiobe_pls?name=eq.Ruby"+          [("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]+          [json| [ { "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" $ 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"]+            }++      it "works with POST and ignore-duplicates headers" $ do+        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|[+              { "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }+            ]|]+            { matchStatus = 201+            , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates"]+            }++      it "works with PUT" $ do+        put "/UnitTest?idUnitTest=eq.1" [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] `shouldRespondWith` 204+        get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith`+          [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|]
+ test/spec/Main.hs view
@@ -0,0 +1,234 @@+module Main where++import qualified Data.Aeson                 as JSON+import qualified Hasql.Pool                 as P+import qualified Hasql.Transaction.Sessions as HT++import Data.Function      (id)+import Data.List.NonEmpty (toList)++import Test.Hspec++import PostgREST.App             (postgrest)+import PostgREST.Config          (AppConfig (..), LogLevel (..))+import PostgREST.Config.Database (queryPgVersion)+import PostgREST.DbStructure     (queryDbStructure)+import Protolude                 hiding (toList, toS)+import Protolude.Conv            (toS)+import SpecHelper++import qualified PostgREST.AppState as AppState++import qualified Feature.AndOrParamsSpec+import qualified Feature.AsymmetricJwtSpec+import qualified Feature.AudienceJwtSecretSpec+import qualified Feature.AuthSpec+import qualified Feature.BinaryJwtSecretSpec+import qualified Feature.ConcurrentSpec+import qualified Feature.CorsSpec+import qualified Feature.DeleteSpec+import qualified Feature.DisabledOpenApiSpec+import qualified Feature.EmbedDisambiguationSpec+import qualified Feature.EmbedInnerJoinSpec+import qualified Feature.ExtraSearchPathSpec+import qualified Feature.HtmlRawOutputSpec+import qualified Feature.IgnorePrivOpenApiSpec+import qualified Feature.InsertSpec+import qualified Feature.JsonOperatorSpec+import qualified Feature.LegacyGucsSpec+import qualified Feature.MultipleSchemaSpec+import qualified Feature.NoJwtSpec+import qualified Feature.NonexistentSchemaSpec+import qualified Feature.OpenApiSpec+import qualified Feature.OptionsSpec+import qualified Feature.ProxySpec+import qualified Feature.QueryLimitedSpec+import qualified Feature.QuerySpec+import qualified Feature.RangeSpec+import qualified Feature.RawOutputTypesSpec+import qualified Feature.RollbackSpec+import qualified Feature.RootSpec+import qualified Feature.RpcPreRequestGucsSpec+import qualified Feature.RpcSpec+import qualified Feature.SingularSpec+import qualified Feature.UnicodeSpec+import qualified Feature.UpdateSpec+import qualified Feature.UpsertSpec+++main :: IO ()+main = do+  testDbConn <- getEnvVarWithDefault "PGRST_DB_URI" "postgres://postgrest_test@localhost/postgrest_test"++  pool <- P.acquire (3, 10, toS testDbConn)++  actualPgVersion <- either (panic.show) id <$> P.use pool queryPgVersion++  baseDbStructure <-+    loadDbStructure pool+      (configDbSchemas $ testCfg testDbConn)+      (configDbExtraSearchPath $ testCfg testDbConn)++  let+    -- For tests that run with the same refDbStructure+    app cfg = do+      let config = cfg testDbConn+      appState <- AppState.initWithPool pool config+      AppState.putPgVersion appState actualPgVersion+      AppState.putDbStructure appState baseDbStructure+      when (isJust $ configDbRootSpec config) $+        AppState.putJsonDbS appState $ toS $ JSON.encode baseDbStructure+      return ((), postgrest LogCrit appState $ pure ())++    -- For tests that run with a different DbStructure(depends on configSchemas)+    appDbs cfg = do+      let config = cfg testDbConn+      customDbStructure <-+        loadDbStructure pool+          (configDbSchemas config)+          (configDbExtraSearchPath config)+      appState <- AppState.initWithPool pool config+      AppState.putPgVersion appState actualPgVersion+      AppState.putDbStructure appState customDbStructure+      when (isJust $ configDbRootSpec config) $+        AppState.putJsonDbS appState $ toS $ JSON.encode baseDbStructure+      return ((), postgrest LogCrit appState $ pure ())++  let withApp              = app testCfg+      maxRowsApp           = app testMaxRowsCfg+      disabledOpenApi      = app testDisabledOpenApiCfg+      proxyApp             = app testProxyCfg+      noJwtApp             = app testCfgNoJWT+      binaryJwtApp         = app testCfgBinaryJWT+      audJwtApp            = app testCfgAudienceJWT+      asymJwkApp           = app testCfgAsymJWK+      asymJwkSetApp        = app testCfgAsymJWKSet+      rootSpecApp          = app testCfgRootSpec+      htmlRawOutputApp     = app testCfgHtmlRawOutput+      responseHeadersApp   = app testCfgResponseHeaders+      disallowRollbackApp  = app testCfgDisallowRollback+      forceRollbackApp     = app testCfgForceRollback+      testCfgLegacyGucsApp = app testCfgLegacyGucs++      extraSearchPathApp   = appDbs testCfgExtraSearchPath+      unicodeApp           = appDbs testUnicodeCfg+      nonexistentSchemaApp = appDbs testNonexistentSchemaCfg+      multipleSchemaApp    = appDbs testMultipleSchemaCfg+      ignorePrivOpenApi    = appDbs testIgnorePrivOpenApiCfg++  let analyze :: IO ()+      analyze = do+        analyzeTable testDbConn "items"+        analyzeTable testDbConn "child_entities"++      specs = uncurry describe <$> [+          ("Feature.AndOrParamsSpec"         , Feature.AndOrParamsSpec.spec actualPgVersion)+        , ("Feature.AuthSpec"                , Feature.AuthSpec.spec actualPgVersion)+        , ("Feature.ConcurrentSpec"          , Feature.ConcurrentSpec.spec)+        , ("Feature.CorsSpec"                , Feature.CorsSpec.spec)+        , ("Feature.DeleteSpec"              , Feature.DeleteSpec.spec)+        , ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)+        , ("Feature.EmbedInnerJoinSpec"      , Feature.EmbedInnerJoinSpec.spec)+        , ("Feature.InsertSpec"              , Feature.InsertSpec.spec actualPgVersion)+        , ("Feature.JsonOperatorSpec"        , Feature.JsonOperatorSpec.spec actualPgVersion)+        , ("Feature.OpenApiSpec"             , Feature.OpenApiSpec.spec actualPgVersion)+        , ("Feature.OptionsSpec"             , Feature.OptionsSpec.spec actualPgVersion)+        , ("Feature.QuerySpec"               , Feature.QuerySpec.spec actualPgVersion)+        , ("Feature.RawOutputTypesSpec"      , Feature.RawOutputTypesSpec.spec)+        , ("Feature.RpcSpec"                 , Feature.RpcSpec.spec actualPgVersion)+        , ("Feature.SingularSpec"            , Feature.SingularSpec.spec)+        , ("Feature.UpdateSpec"              , Feature.UpdateSpec.spec)+        , ("Feature.UpsertSpec"              , Feature.UpsertSpec.spec actualPgVersion)+        ]++  hspec $ do+    mapM_ (parallel . before withApp) specs++    -- we analyze to get accurate results from EXPLAIN+    parallel $ beforeAll_ analyze . before withApp $+      describe "Feature.RangeSpec" Feature.RangeSpec.spec++    -- this test runs with a raw-output-media-types set to text/html+    parallel $ before htmlRawOutputApp $+      describe "Feature.HtmlRawOutputSpec" Feature.HtmlRawOutputSpec.spec++    -- this test runs with a different server flag+    parallel $ before maxRowsApp $+      describe "Feature.QueryLimitedSpec" Feature.QueryLimitedSpec.spec++    -- this test runs with a different schema+    parallel $ before unicodeApp $+      describe "Feature.UnicodeSpec" Feature.UnicodeSpec.spec++    -- this test runs with openapi-mode set to disabled+    parallel $ before disabledOpenApi $+      describe "Feature.DisabledOpenApiSpec" Feature.DisabledOpenApiSpec.spec++    -- this test runs with openapi-mode set to ignore-acl+    parallel $ before ignorePrivOpenApi $+      describe "Feature.IgnorePrivOpenApiSpec" Feature.IgnorePrivOpenApiSpec.spec++    -- this test runs with a proxy+    parallel $ before proxyApp $+      describe "Feature.ProxySpec" Feature.ProxySpec.spec++    -- this test runs without a JWT secret+    parallel $ before noJwtApp $+      describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec++    -- this test runs with a binary JWT secret+    parallel $ before binaryJwtApp $+      describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec++    -- this test runs with a binary JWT secret and an audience claim+    parallel $ before audJwtApp $+      describe "Feature.AudienceJwtSecretSpec" Feature.AudienceJwtSecretSpec.spec++    -- this test runs with asymmetric JWK+    parallel $ before asymJwkApp $+      describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec++    -- this test runs with asymmetric JWKSet+    parallel $ before asymJwkSetApp $+      describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec++    -- this test runs with a nonexistent db-schema+    parallel $ before nonexistentSchemaApp $+      describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec++    -- this test runs with an extra search path+    parallel $ before extraSearchPathApp $+      describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec++    -- this test runs with a root spec function override+    parallel $ before rootSpecApp $+      describe "Feature.RootSpec" Feature.RootSpec.spec+    parallel $ before responseHeadersApp $+      describe "Feature.RpcPreRequestGucsSpec" Feature.RpcPreRequestGucsSpec.spec++    -- this test runs with multiple schemas+    parallel $ before multipleSchemaApp $+      describe "Feature.MultipleSchemaSpec" Feature.MultipleSchemaSpec.spec++    -- this test runs with db-uses-legacy-gucs = false+    parallel $ before testCfgLegacyGucsApp $+      describe "Feature.LegacyGucsSpec" Feature.LegacyGucsSpec.spec++    -- Note: the rollback tests can not run in parallel, because they test persistance and+    -- this results in race conditions++    -- this test runs with tx-rollback-all = true and tx-allow-override = true+    before withApp $+      describe"Feature.RollbackAllowedSpec" Feature.RollbackSpec.allowed++    -- this test runs with tx-rollback-all = false and tx-allow-override = false+    before disallowRollbackApp $+      describe "Feature.RollbackDisallowedSpec" Feature.RollbackSpec.disallowed++    -- this test runs with tx-rollback-all = true and tx-allow-override = false+    before forceRollbackApp $+      describe "Feature.RollbackForcedSpec" Feature.RollbackSpec.forced++  where+    loadDbStructure pool schemas extraSearchPath =+      either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ queryDbStructure (toList schemas) extraSearchPath True)
+ test/spec/QueryCost.hs view
@@ -0,0 +1,89 @@+module Main where++import           Control.Lens                      ((^?))+import qualified Data.Aeson.Lens                   as L+import qualified Hasql.Decoders                    as HD+import qualified Hasql.DynamicStatements.Snippet   as H+import qualified Hasql.DynamicStatements.Statement as H+import qualified Hasql.Pool                        as P+import qualified Hasql.Statement                   as H+import qualified Hasql.Transaction                 as HT+import qualified Hasql.Transaction.Sessions        as HT+import           Text.Heredoc++import Protolude      hiding (get, toS)+import Protolude.Conv (toS)++import PostgREST.Query.QueryBuilder (requestToCallProcQuery)+import PostgREST.Request.Types++import PostgREST.DbStructure.Identifiers+import PostgREST.DbStructure.Proc++import SpecHelper (getEnvVarWithDefault)++import Test.Hspec++main :: IO ()+main = do+  testDbConn <- getEnvVarWithDefault "PGRST_DB_URI" "postgres://postgrest_test@localhost/postgrest_test"+  pool <- P.acquire (3, 10, toS testDbConn)++  hspec $ describe "QueryCost" $+    context "call proc query" $ do+      it "should not exceed cost when calling setof composite proc" $ do+        cost <- exec pool $+          requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")+          (KeyParams [ProcParam "id" "int" True False])+          (Just [str| {"id": 3} |]) False False [])+        liftIO $+          cost `shouldSatisfy` (< Just 40)++      it "should not exceed cost when calling setof composite proc with empty params" $ do+        cost <- exec pool $+          requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") (KeyParams []) Nothing False False [])+        liftIO $+          cost `shouldSatisfy` (< Just 30)++      it "should not exceed cost when calling scalar proc" $ do+        cost <- exec pool $+          requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")+          (KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])+          (Just [str| {"a": 3, "b": 4} |]) True False [])+        liftIO $+          cost `shouldSatisfy` (< Just 10)++      context "params=multiple-objects" $ do+        it "should not exceed cost when calling setof composite proc" $ do+          cost <- exec pool $+            requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")+            (KeyParams [ProcParam "id" "int" True False])+            (Just [str| [{"id": 1}, {"id": 4}] |]) False True [])+          liftIO $ do+            -- lower bound needed for now to make sure that cost is not Nothing+            cost `shouldSatisfy` (> Just 2000)+            cost `shouldSatisfy` (< Just 2100)++        it "should not exceed cost when calling scalar proc" $ do+          cost <- exec pool $+            requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")+            (KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])+            (Just [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True False [])+          liftIO $+            cost `shouldSatisfy` (< Just 10)+++exec :: P.Pool -> H.Snippet -> IO (Maybe Int64)+exec pool query =+  join . rightToMaybe <$>+  P.use pool (HT.transaction HT.ReadCommitted HT.Read $ HT.statement mempty $ explainCost query)++explainCost :: H.Snippet -> H.Statement () (Maybe Int64)+explainCost query =+  H.dynamicallyParameterized snippet decodeExplain False+ where+   snippet = "EXPLAIN (FORMAT JSON) " <> query+   decodeExplain :: HD.Result (Maybe Int64)+   decodeExplain =+     let row = HD.singleRow $ HD.column $ HD.nonNullable HD.bytea in+     (^? L.nth 0 . L.key "Plan" .  L.key "Total Cost" . L._Integral) <$> row
+ test/spec/SpecHelper.hs view
@@ -0,0 +1,235 @@+module SpecHelper where++import qualified Data.ByteString.Base64 as B64 (decodeLenient)+import qualified Data.ByteString.Char8  as BS+import qualified Data.ByteString.Lazy   as BL+import qualified Data.Map.Strict        as M+import qualified Data.Set               as S+import qualified System.IO.Error        as E++import Data.Aeson           (Value (..), decode, encode)+import Data.CaseInsensitive (CI (..), original)+import Data.List            (lookup)+import Data.List.NonEmpty   (fromList)+import Network.Wai.Test     (SResponse (simpleBody, simpleHeaders, simpleStatus))+import System.Environment   (getEnv)+import System.Process       (readProcess)+import Text.Regex.TDFA      ((=~))+++import Network.HTTP.Types+import Test.Hspec+import Test.Hspec.Wai+import Text.Heredoc++import PostgREST.Config                  (AppConfig (..),+                                          JSPathExp (..),+                                          LogLevel (..),+                                          OpenAPIMode (..),+                                          parseSecret)+import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))+import Protolude                         hiding (toS)+import Protolude.Conv                    (toS)++matchContentTypeJson :: MatchHeader+matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8"++matchContentTypeSingular :: MatchHeader+matchContentTypeSingular = "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"++matchHeaderAbsent :: HeaderName -> MatchHeader+matchHeaderAbsent name = MatchHeader $ \headers _body ->+  case lookup name headers of+    Just _  -> Just $ "unexpected header: " <> toS (original name) <> "\n"+    Nothing -> Nothing++validateOpenApiResponse :: [Header] -> WaiSession () ()+validateOpenApiResponse headers = do+  r <- request methodGet "/" headers ""+  liftIO $+    let respStatus = simpleStatus r in+    respStatus `shouldSatisfy`+      \s -> s == Status { statusCode = 200, statusMessage="OK" }+  liftIO $+    let respHeaders = simpleHeaders r in+    respHeaders `shouldSatisfy`+      \hs -> ("Content-Type", "application/openapi+json; charset=utf-8") `elem` hs+  let Just body = decode (simpleBody r)+  Just schema <- liftIO $ decode <$> BL.readFile "test/spec/fixtures/openapi.json"+  let args :: M.Map Text Value+      args = M.fromList+        [ ( "schema", schema )+        , ( "data", body ) ]+      hdrs = acceptHdrs "application/json"+  request methodPost "/rpc/validate_json_schema" hdrs (encode args)+      `shouldRespondWith` "true"+      { matchStatus = 200+      , matchHeaders = []+      }+++getEnvVarWithDefault :: Text -> Text -> IO Text+getEnvVarWithDefault var def = toS <$>+  getEnv (toS var) `E.catchIOError` const (return $ toS def)++_baseCfg :: AppConfig+_baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in+  AppConfig {+    configAppSettings           = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]+  , configDbAnonRole            = "postgrest_test_anonymous"+  , configDbChannel             = mempty+  , configDbChannelEnabled      = True+  , configDbExtraSearchPath     = []+  , configDbMaxRows             = Nothing+  , configDbPoolSize            = 10+  , configDbPoolTimeout         = 10+  , configDbPreRequest          = Just $ QualifiedIdentifier "test" "switch_role"+  , configDbPreparedStatements  = True+  , configDbRootSpec            = Nothing+  , configDbSchemas             = fromList ["test"]+  , configDbConfig              = False+  , configDbUri                 = mempty+  , configDbUseLegacyGucs       = True+  , configFilePath              = Nothing+  , configJWKS                  = parseSecret <$> secret+  , configJwtAudience           = Nothing+  , configJwtRoleClaimKey       = [JSPKey "role"]+  , configJwtSecret             = secret+  , configJwtSecretIsBase64     = False+  , configLogLevel              = LogCrit+  , configOpenApiMode           = OAFollowPriv+  , configOpenApiServerProxyUri = Nothing+  , configRawMediaTypes         = []+  , configServerHost            = "localhost"+  , configServerPort            = 3000+  , configServerUnixSocket      = Nothing+  , configServerUnixSocketMode  = 432+  , configDbTxAllowOverride     = True+  , configDbTxRollbackAll       = True+  }++testCfg :: Text -> AppConfig+testCfg testDbConn = _baseCfg { configDbUri = testDbConn }++testCfgDisallowRollback :: Text -> AppConfig+testCfgDisallowRollback testDbConn = (testCfg testDbConn) { configDbTxAllowOverride = False, configDbTxRollbackAll = False }++testCfgForceRollback :: Text -> AppConfig+testCfgForceRollback testDbConn = (testCfg testDbConn) { configDbTxAllowOverride = False, configDbTxRollbackAll = True }++testCfgNoJWT :: Text -> AppConfig+testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing, configJWKS = Nothing }++testUnicodeCfg :: Text -> AppConfig+testUnicodeCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["تست"] }++testMaxRowsCfg :: Text -> AppConfig+testMaxRowsCfg testDbConn = (testCfg testDbConn) { configDbMaxRows = Just 2 }++testDisabledOpenApiCfg :: Text -> AppConfig+testDisabledOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OADisabled }++testIgnorePrivOpenApiCfg :: Text -> AppConfig+testIgnorePrivOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OAIgnorePriv, configDbSchemas = fromList ["test", "v1"] }++testProxyCfg :: Text -> AppConfig+testProxyCfg testDbConn = (testCfg testDbConn) { configOpenApiServerProxyUri = Just "https://postgrest.com/openapi.json" }++testCfgBinaryJWT :: Text -> AppConfig+testCfgBinaryJWT testDbConn =+  let secret = Just . B64.decodeLenient $ "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" in+  (testCfg testDbConn) {+    configJwtSecret = secret+  , configJWKS = parseSecret <$> secret+  }++testCfgAudienceJWT :: Text -> AppConfig+testCfgAudienceJWT testDbConn =+  let secret = Just . B64.decodeLenient $ "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" in+  (testCfg testDbConn) {+    configJwtSecret = secret+  , configJwtAudience = Just "youraudience"+  , configJWKS = parseSecret <$> secret+  }++testCfgAsymJWK :: Text -> AppConfig+testCfgAsymJWK testDbConn =+  let secret = Just $ encodeUtf8 [str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|]+  in (testCfg testDbConn) {+    configJwtSecret = secret+  , configJWKS = parseSecret <$> secret+  }++testCfgAsymJWKSet :: Text -> AppConfig+testCfgAsymJWKSet testDbConn =+  let secret = Just $ encodeUtf8 [str|{"keys": [{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}]}|]+  in (testCfg testDbConn) {+    configJwtSecret = secret+  , configJWKS = parseSecret <$> secret+  }++testNonexistentSchemaCfg :: Text -> AppConfig+testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["nonexistent"] }++testCfgExtraSearchPath :: Text -> AppConfig+testCfgExtraSearchPath testDbConn = (testCfg testDbConn) { configDbExtraSearchPath = ["public", "extensions"] }++testCfgRootSpec :: Text -> AppConfig+testCfgRootSpec testDbConn = (testCfg testDbConn) { configDbRootSpec = Just $ QualifiedIdentifier mempty "root"}++testCfgHtmlRawOutput :: Text -> AppConfig+testCfgHtmlRawOutput testDbConn = (testCfg testDbConn) { configRawMediaTypes = ["text/html"] }++testCfgResponseHeaders :: Text -> AppConfig+testCfgResponseHeaders testDbConn = (testCfg testDbConn) { configDbPreRequest = Just $ QualifiedIdentifier mempty "custom_headers" }++testMultipleSchemaCfg :: Text -> AppConfig+testMultipleSchemaCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["v1", "v2"] }++testCfgLegacyGucs :: Text -> AppConfig+testCfgLegacyGucs testDbConn = (testCfg testDbConn) { configDbUseLegacyGucs = False }++analyzeTable :: Text -> Text -> IO ()+analyzeTable dbConn tableName =+  void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", toS dbConn, "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] []++rangeHdrs :: ByteRange -> [Header]+rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]++rangeHdrsWithCount :: ByteRange -> [Header]+rangeHdrsWithCount r = ("Prefer", "count=exact") : rangeHdrs r++acceptHdrs :: BS.ByteString -> [Header]+acceptHdrs mime = [(hAccept, mime)]++rangeUnit :: Header+rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")++matchHeader :: CI BS.ByteString -> BS.ByteString -> [Header] -> Bool+matchHeader name valRegex headers =+  maybe False (=~ valRegex) $ lookup name headers++noBlankHeader :: [Header] -> Bool+noBlankHeader = notElem mempty++noProfileHeader :: [Header] -> Bool+noProfileHeader headers = isNothing $ find ((== "Content-Profile") . fst) headers++authHeader :: BS.ByteString -> BS.ByteString -> Header+authHeader typ creds =+  (hAuthorization, typ <> " " <> creds)++authHeaderJWT :: BS.ByteString -> Header+authHeaderJWT = authHeader "Bearer"++-- | Tests whether the text can be parsed as a json object containing+-- the key "message", and optional keys "details", "hint", "code",+-- and no extraneous keys+isErrorFormat :: BL.ByteString -> Bool+isErrorFormat s =+  "message" `S.member` keys &&+    S.null (S.difference keys validKeys)+ where+  obj = decode s :: Maybe (M.Map Text Value)+  keys = maybe S.empty M.keysSet obj+  validKeys = S.fromList ["message", "details", "hint", "code"]
+ test/spec/TestTypes.hs view
@@ -0,0 +1,37 @@+module TestTypes (+  IncPK(..)+, CompoundPK(..)+) where++import           Data.Aeson ((.:))+import qualified Data.Aeson as JSON++import Protolude++data IncPK = IncPK {+  incId          :: Int+, incNullableStr :: Maybe Text+, incStr         :: Text+, incInsert      :: Text+} deriving (Eq, Show)++instance JSON.FromJSON IncPK where+  parseJSON (JSON.Object r) = IncPK <$>+    r .: "id" <*>+    r .: "nullable_string" <*>+    r .: "non_nullable_string" <*>+    r .: "inserted_at"+  parseJSON _ = mzero++data CompoundPK = CompoundPK {+  compoundK1    :: Int+, compoundK2    :: Text+, compoundExtra :: Maybe Int+} deriving (Eq, Show)++instance JSON.FromJSON CompoundPK where+  parseJSON (JSON.Object r) = CompoundPK <$>+    r .: "k1" <*>+    r .: "k2" <*>+    r .: "extra"+  parseJSON _ = mzero