diff --git a/postgrest.cabal b/postgrest.cabal index d623df7aa..8aeb74145 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -58,7 +58,8 @@ library if flag(ci) ghc-options: -Wall -W -Werror else - ghc-options: -Wall -W -O2 + -- ghc-options: -Wall -W -O2 + ghc-options: -Wall -W default-language: Haskell2010 default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes @@ -88,6 +89,7 @@ library , cassava , jwt Exposed-Modules: PostgREST.App + , PostgREST.Types , PostgREST.Auth , PostgREST.Config , PostgREST.Error @@ -108,6 +110,7 @@ Test-Suite spec ghc-options: -Wall -W -O2 Main-Is: Main.hs Other-Modules: PostgREST.App + , PostgREST.Types , PostgREST.Auth , PostgREST.Config , PostgREST.Error diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index b4df142fd..a702752e3 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -1,11 +1,16 @@ {-# LANGUAGE FlexibleContexts #-} -module PostgREST.App (app, sqlError, isSqlError, contentTypeForAccept) where +module PostgREST.App (app, sqlError, isSqlError, contentTypeForAccept +-- added +, jsonH +, requestedSchema +, TableOptions(..) +) where import Control.Monad (join) import Control.Arrow ((***), second) import Control.Applicative -import Data.Text hiding (map, find) +import Data.Text hiding (map, find, filter) import Data.Maybe (fromMaybe, mapMaybe, isJust, isNothing) import Text.Regex.TDFA ((=~)) import Data.Ord (comparing) @@ -36,6 +41,7 @@ import qualified Hasql as H import qualified Hasql.Backend as B import qualified Hasql.Postgres as P +import PostgREST.Types import PostgREST.Config (AppConfig(..)) import PostgREST.Auth import PostgREST.PgQuery @@ -44,19 +50,31 @@ import PostgREST.PgStructure import Prelude -app :: AppConfig -> BL.ByteString -> Request -> H.Tx P.Postgres s Response -app conf reqBody req = +app :: [Table] -> [Relation] -> [Column] -> [PrimaryKey] -> AppConfig -> BL.ByteString -> Request -> H.Tx P.Postgres s Response +app allTables allRelations allColumns allPrimaryKeys conf reqBody req = case (path, verb) of + -- ([], _) -> do + -- body <- encode <$> tables (cs schema) + -- return $ responseLBS status200 [jsonH] $ cs body + + -- ([table], "OPTIONS") -> do + -- let qt = qualify table + -- cols <- columns qt + -- pkey <- map cs <$> primaryKeyColumns qt + -- return $ responseLBS status200 [jsonH, allOrigins] + -- $ encode (TableOptions cols pkey) + ([], _) -> do - body <- encode <$> tables (cs schema) - return $ responseLBS status200 [jsonH] $ cs body + let body = encode $ filter (((cs schema)==).tableSchema) allTables + return $ responseLBS status200 [jsonH, ("Custom", "header")] $ cs body ([table], "OPTIONS") -> do - let qt = qualify table - cols <- columns qt - pkey <- map cs <$> primaryKeyColumns qt - return $ responseLBS status200 [jsonH, allOrigins] - $ encode (TableOptions cols pkey) + let qt = Table schema table + let cols = filter (filterCol schema table) allColumns + let pkey = map pkName $ filter (filterPk schema table) allPrimaryKeys + let body = encode (TableOptions cols pkey) + return $ responseLBS status200 [jsonH, allOrigins, ("Custom", "header2")] $ cs body + ([table], "GET") -> if range == Just emptyRange @@ -187,7 +205,8 @@ app conf reqBody req = then return $ responseLBS status405 [] "You must speficy all and only primary keys as params" else do - tableCols <- map (cs . colName) <$> columns qt + --tableCols <- map (cs . colName) <$> columns qt + let tableCols = map (cs . colName) $ filter (filterCol schema table) allColumns let cols = map cs $ M.keys obj if S.fromList tableCols == S.fromList cols then do @@ -238,6 +257,8 @@ app conf reqBody req = return $ responseLBS status404 [] "" where + filterCol schema table (Column{colSchema=s, colTable=t}) = s==schema && table==t + filterPk schema table (PrimaryKey{pkSchema=s, pkTable=t}) = s==schema && table==t path = pathInfo req verb = requestMethod req qq = queryString req diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 37c772e37..1de7da33b 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -28,10 +28,10 @@ data AppConfig = AppConfig { argParser :: Parser AppConfig argParser = AppConfig - <$> strOption (long "db-name" <> short 'd' <> metavar "NAME" <> help "name of database") + <$> strOption (long "db-name" <> short 'd' <> metavar "NAME" <> value "skin_test" <> help "name of database") <*> option auto (long "db-port" <> short 'P' <> metavar "PORT" <> value 5432 <> help "postgres server port" <> showDefault) - <*> strOption (long "db-user" <> short 'U' <> metavar "ROLE" <> help "postgres authenticator role") - <*> strOption (long "db-pass" <> metavar "PASS" <> value "" <> help "password for authenticator role") + <*> strOption (long "db-user" <> short 'U' <> metavar "ROLE" <> value "skin_test" <> help "postgres authenticator role") + <*> strOption (long "db-pass" <> metavar "PASS" <> value "skin_pass" <> help "password for authenticator role") <*> strOption (long "db-host" <> metavar "HOST" <> value "localhost" <> help "postgres server hostname" <> showDefault) <*> option auto (long "port" <> short 'p' <> metavar "PORT" <> value 3000 <> help "port number on which to run HTTP server" <> showDefault) diff --git a/src/PostgREST/Main.hs b/src/PostgREST/Main.hs index a0d486c29..83842ae4b 100644 --- a/src/PostgREST/Main.hs +++ b/src/PostgREST/Main.hs @@ -2,6 +2,16 @@ module Main where import Paths_postgrest (version) +-- added +import PostgREST.PgStructure +import Data.Aeson +import Data.List (find) +import Data.Maybe (isJust) +import PostgREST.Types +import Network.HTTP.Types.Status +import Network.HTTP.Types.Header +import Network.Wai -- (strictRequestBody, pathInfo, requestMethod, requestHeaders) + import PostgREST.App import PostgREST.Middleware @@ -10,7 +20,6 @@ import PostgREST.Error(errResponse) import Control.Monad (unless) import Control.Monad.IO.Class (liftIO) import Data.String.Conversions (cs) -import Network.Wai (strictRequestBody) import Network.Wai.Handler.Warp hiding (Connection) import Network.Wai.Middleware.RequestLogger (logStdout) import Data.List (intercalate) @@ -74,10 +83,26 @@ main = do fail "Cannot run in this PostgreSQL version, PostgREST needs at least 9.2.0" ) resOrError + -- read the structure of the database + -- read the structure of the database + let txParam = (Just (H.ReadCommitted, Just True)) + + tblsRes <- H.session pool $ H.tx txParam alltables + let allTables = either (fail . show) id tblsRes + + relsRes <- H.session pool $ H.tx txParam allrelations + let allRelations = either (fail . show) id relsRes + + colsRes <- H.session pool $ H.tx txParam $ allcolumns allRelations + let allColumns = either (fail . show) id colsRes + + pkRes <- H.session pool $ H.tx txParam $ allprimaryKeys + let allPrimaryKeys = either (fail . show) id pkRes + runSettings appSettings $ middle $ \req respond -> do body <- strictRequestBody req resOrError <- liftIO $ H.session pool $ H.tx (Just (H.ReadCommitted, Just True)) $ - authenticated conf (app conf body) req + authenticated conf (app allTables allRelations allColumns allPrimaryKeys conf body) req either (respond . errResponse) respond resOrError where diff --git a/src/PostgREST/PgStructure.hs b/src/PostgREST/PgStructure.hs index 9e5647b8a..dc0e52de9 100644 --- a/src/PostgREST/PgStructure.hs +++ b/src/PostgREST/PgStructure.hs @@ -4,7 +4,9 @@ module PostgREST.PgStructure where import PostgREST.PgQuery (QualifiedIdentifier(..)) -import Data.Text hiding (foldl, map, zipWith, concat) +import PostgREST.Types +import Data.Text (Text, unpack, split) +import Data.List (find) import Data.Aeson import Data.Functor.Identity import Data.String.Conversions (cs) @@ -18,93 +20,94 @@ import qualified Hasql.Postgres as P import Prelude -foreignKeys :: QualifiedIdentifier -> H.Tx P.Postgres s (Map.Map Text ForeignKey) -foreignKeys table = do - r <- H.listEx $ [H.stmt| - select kcu.column_name, ccu.table_name AS foreign_table_name, - ccu.column_name AS foreign_column_name - from information_schema.table_constraints AS tc - join information_schema.key_column_usage AS kcu - on tc.constraint_name = kcu.constraint_name - join information_schema.constraint_column_usage AS ccu - on ccu.constraint_name = tc.constraint_name - where constraint_type = 'FOREIGN KEY' - and tc.table_name=? and tc.table_schema = ? - order by kcu.column_name - |] (qiName table) (qiSchema table) - - return $ foldl addKey Map.empty r - where - addKey :: Map.Map Text ForeignKey -> (Text, Text, Text) -> Map.Map Text ForeignKey - addKey m (col, ftab, fcol) = Map.insert col (ForeignKey ftab fcol) m +----------- +-- foreignKeys :: QualifiedIdentifier -> H.Tx P.Postgres s (Map.Map Text ForeignKey) +-- foreignKeys table = do +-- r <- H.listEx $ [H.stmt| +-- select kcu.column_name, ccu.table_name AS foreign_table_name, +-- ccu.column_name AS foreign_column_name +-- from information_schema.table_constraints AS tc +-- join information_schema.key_column_usage AS kcu +-- on tc.constraint_name = kcu.constraint_name +-- join information_schema.constraint_column_usage AS ccu +-- on ccu.constraint_name = tc.constraint_name +-- where constraint_type = 'FOREIGN KEY' +-- and tc.table_name=? and tc.table_schema = ? +-- order by kcu.column_name +-- |] (qiName table) (qiSchema table) +-- +-- return $ foldl addKey Map.empty r +-- where +-- addKey :: Map.Map Text ForeignKey -> (Text, Text, Text) -> Map.Map Text ForeignKey +-- addKey m (col, ftab, fcol) = Map.insert col (ForeignKey ftab fcol) m +-- +-- +-- tables :: Text -> H.Tx P.Postgres s [Table] +-- tables schema = do +-- rows <- H.listEx $ +-- [H.stmt| +-- select +-- n.nspname as table_schema, +-- relname as table_name, +-- c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8 +-- or (exists ( +-- select 1 +-- from pg_trigger +-- where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69) +-- ) as insertable +-- from +-- pg_class c +-- join pg_namespace n on n.oid = c.relnamespace +-- where +-- c.relkind in ('v', 'r', 'm') +-- and n.nspname = ? +-- and ( +-- pg_has_role(c.relowner, 'USAGE'::text) +-- or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) +-- or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text) +-- ) +-- order by relname +-- |] schema +-- return $ map tableFromRow rows -tables :: Text -> H.Tx P.Postgres s [Table] -tables schema = do - rows <- H.listEx $ - [H.stmt| - select - n.nspname as table_schema, - relname as table_name, - c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8 - or (exists ( - select 1 - from pg_trigger - where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69) - ) as insertable - from - pg_class c - join pg_namespace n on n.oid = c.relnamespace - where - c.relkind in ('v', 'r', 'm') - and n.nspname = ? - and ( - pg_has_role(c.relowner, 'USAGE'::text) - or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) - or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text) - ) - order by relname - |] schema - return $ map tableFromRow rows - - -columns :: QualifiedIdentifier -> H.Tx P.Postgres s [Column] -columns table = do - cols <- H.listEx $ [H.stmt| - select info.table_schema as schema, info.table_name as table_name, - info.column_name as name, info.ordinal_position as position, - info.is_nullable::boolean as nullable, info.data_type as col_type, - info.is_updatable::boolean as updatable, - info.character_maximum_length as max_len, - info.numeric_precision as precision, - info.column_default as default_value, - array_to_string(enum_info.vals, ',') as enum - from ( - select table_schema, table_name, column_name, ordinal_position, - is_nullable, data_type, is_updatable, - character_maximum_length, numeric_precision, - column_default, udt_name - from information_schema.columns - where table_schema = ? and table_name = ? - ) as info - left outer join ( - select n.nspname as s, - t.typname as n, - array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals - from pg_type t - join pg_enum e on t.oid = e.enumtypid - join pg_catalog.pg_namespace n ON n.oid = t.typnamespace - group by s, n - ) as enum_info - on (info.udt_name = enum_info.n) - order by position |] - (qiSchema table) (qiName table) - - fks <- foreignKeys table - return $ map (addFK fks . columnFromRow) cols - - where - addFK fks col = col { colFK = Map.lookup (cs . colName $ col) fks } +-- columns :: QualifiedIdentifier -> H.Tx P.Postgres s [Column] +-- columns table = do +-- cols <- H.listEx $ [H.stmt| +-- select info.table_schema as schema, info.table_name as table_name, +-- info.column_name as name, info.ordinal_position as position, +-- info.is_nullable::boolean as nullable, info.data_type as col_type, +-- info.is_updatable::boolean as updatable, +-- info.character_maximum_length as max_len, +-- info.numeric_precision as precision, +-- info.column_default as default_value, +-- array_to_string(enum_info.vals, ',') as enum +-- from ( +-- select table_schema, table_name, column_name, ordinal_position, +-- is_nullable, data_type, is_updatable, +-- character_maximum_length, numeric_precision, +-- column_default, udt_name +-- from information_schema.columns +-- where table_schema = ? and table_name = ? +-- ) as info +-- left outer join ( +-- select n.nspname as s, +-- t.typname as n, +-- array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals +-- from pg_type t +-- join pg_enum e on t.oid = e.enumtypid +-- join pg_catalog.pg_namespace n ON n.oid = t.typnamespace +-- group by s, n +-- ) as enum_info +-- on (info.udt_name = enum_info.n) +-- order by position |] +-- (qiSchema table) (qiName table) +-- +-- fks <- foreignKeys table +-- return $ map (addFK fks . columnFromRow) cols +-- +-- where +-- addFK fks col = col { colFK = Map.lookup (cs . colName $ col) fks } primaryKeyColumns :: QualifiedIdentifier -> H.Tx P.Postgres s [Text] @@ -134,30 +137,6 @@ doesProcExist schema proc = do |] schema proc return $ isJust row -data Table = Table { - tableSchema :: Text -, tableName :: Text -, tableInsertable :: Bool -} deriving (Show) - -data ForeignKey = ForeignKey { - fkTable::Text, fkCol::Text -} deriving (Eq, Show) - -data Column = Column { - colSchema :: Text -, colTable :: Text -, colName :: Text -, colPosition :: Int -, colNullable :: Bool -, colType :: Text -, colUpdatable :: Bool -, colMaxLen :: Maybe Int -, colPrecision :: Maybe Int -, colDefault :: Maybe Text -, colEnum :: [Text] -, colFK :: Maybe ForeignKey -} deriving (Show) tableFromRow :: (Text, Text, Bool) -> Table tableFromRow (s, n, i) = Table s n i @@ -197,3 +176,138 @@ instance ToJSON Table where "schema" .= tableSchema v , "name" .= tableName v , "insertable" .= tableInsertable v ] +------------ + + +relationFromRow :: (Text, Text, Text, Text, Text) -> Relation +relationFromRow (s, t, c, ft, fc) = Relation s t c ft fc "child" + +pkFromRow :: (Text, Text, Text) -> PrimaryKey +pkFromRow (s, t, n) = PrimaryKey s t n + + +addFlippedRelation :: Relation -> [Relation] -> [Relation] +addFlippedRelation rel@(Relation s t c ft fc _) rels = Relation s ft fc t c "parent":rel:rels + +alltables :: H.Tx P.Postgres s [Table] +alltables = do + rows <- H.listEx $ [H.stmt| + SELECT n.nspname AS table_schema, + relname AS TABLE_NAME, + c.relkind = 'r' OR (c.relkind IN ('v','f')) + AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8 + OR (EXISTS ( SELECT 1 + FROM pg_trigger + WHERE pg_trigger.tgrelid = c.oid + AND (pg_trigger.tgtype::integer & 69) = 69) + ) AS insertable + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','r','m') + AND n.nspname NOT IN ('information_schema','pg_catalog') + AND ( pg_has_role(c.relowner, 'USAGE'::text) + OR has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) + OR has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text) + ) + ORDER BY relname + |] + return $ map tableFromRow rows + +allrelations :: H.Tx P.Postgres s [Relation] +allrelations = do + rels <- H.listEx $ [H.stmt| + WITH table_fk AS ( + SELECT DISTINCT + tc.table_schema, tc.table_name, kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu on tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu on ccu.constraint_name = tc.constraint_name + WHERE constraint_type = 'FOREIGN KEY' + AND tc.table_schema NOT IN ('pg_catalog', 'information_schema') + ORDER BY tc.table_schema, tc.table_name, kcu.column_name + ) + SELECT * FROM table_fk + UNION + ( + SELECT DISTINCT + vcu.table_schema, vcu.view_name AS table_name, vcu.column_name, + table_fk.foreign_table_name, + table_fk.foreign_column_name + FROM information_schema.view_column_usage as vcu + JOIN table_fk ON + table_fk.table_schema = vcu.view_schema AND + table_fk.table_name = vcu.table_name AND + table_fk.column_name = vcu.column_name + WHERE vcu.view_schema NOT IN ('pg_catalog', 'information_schema') + ORDER BY vcu.table_schema, vcu.view_name, vcu.column_name + ) + + |] + return $ foldr (addFlippedRelation.relationFromRow) [] rels + +allcolumns :: [Relation] -> H.Tx P.Postgres s [Column] +allcolumns relations = do + cols <- H.listEx $ [H.stmt| + SELECT + info.table_schema AS schema, + info.table_name AS table_name, + info.column_name AS name, + info.ordinal_position AS position, + info.is_nullable::boolean AS nullable, + info.data_type AS col_type, + info.is_updatable::boolean AS updatable, + info.character_maximum_length AS max_len, + info.numeric_precision AS precision, + info.column_default AS default_value, + array_to_string(enum_info.vals, ',') AS enum + FROM ( + SELECT + table_schema, + table_name, + column_name, + ordinal_position, + is_nullable, + data_type, + is_updatable, + character_maximum_length, + numeric_precision, + column_default, + udt_name + FROM information_schema.columns + WHERE table_schema NOT IN ('pg_catalog', 'information_schema') + ) AS info + LEFT OUTER JOIN ( + SELECT + n.nspname AS s, + t.typname AS n, + array_agg(e.enumlabel ORDER BY e.enumsortorder) AS vals + FROM pg_type t + JOIN pg_enum e ON t.oid = e.enumtypid + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + GROUP BY s,n + ) AS enum_info ON (info.udt_name = enum_info.n) + ORDER BY schema, position + |] + return $ map (addFK . columnFromRow) cols + + where + addFK col = col { colFK = relToFk <$> find (lookupFn col) relations } + lookupFn (Column{colSchema=cs, colTable=ct, colName=cn}) (Relation{relSchema=rs, relTable=rt, relColumn=rc, relType=rty}) = + cs==rs && ct==rt && cn==rc && rty=="child" + relToFk (Relation{relFTable=t, relFColumn=c}) = ForeignKey t c + +allprimaryKeys :: H.Tx P.Postgres s [PrimaryKey] +allprimaryKeys = do + pks <- H.listEx $ [H.stmt| + SELECT kc.table_schema, kc.table_name, kc.column_name + FROM information_schema.table_constraints tc, + information_schema.key_column_usage kc + WHERE tc.constraint_type = 'PRIMARY KEY' + AND kc.table_name = tc.table_name + AND kc.table_schema = tc.table_schema + AND kc.constraint_name = tc.constraint_name + AND kc.table_schema NOT IN ('pg_catalog', 'information_schema') + |] + return $ map pkFromRow pks diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs new file mode 100644 index 000000000..e1ad82f6f --- /dev/null +++ b/src/PostgREST/Types.hs @@ -0,0 +1,41 @@ +module PostgREST.Types where +import Data.Text + +data Table = Table { + tableSchema :: Text +, tableName :: Text +, tableInsertable :: Bool +} deriving (Show) + +data ForeignKey = ForeignKey { + fkTable::Text, fkCol::Text +} deriving (Eq, Show) + + +data Column = Column { + colSchema :: Text +, colTable :: Text +, colName :: Text +, colPosition :: Int +, colNullable :: Bool +, colType :: Text +, colUpdatable :: Bool +, colMaxLen :: Maybe Int +, colPrecision :: Maybe Int +, colDefault :: Maybe Text +, colEnum :: [Text] +, colFK :: Maybe ForeignKey +} deriving (Show) + +data PrimaryKey = PrimaryKey { + pkSchema::Text, pkTable::Text, pkName::Text +} + +data Relation = Relation { + relSchema :: Text +, relTable :: Text +, relColumn :: Text +, relFTable :: Text +, relFColumn :: Text +, relType :: Text +} deriving (Show, Eq) diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index f198cf7dc..6fbfee02f 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -30,6 +30,7 @@ import PostgREST.App (app) import PostgREST.Config (AppConfig(..)) import PostgREST.Middleware import PostgREST.Error(errResponse) +import PostgREST.PgStructure isLeft :: Either a b -> Bool isLeft (Left _ ) = True @@ -53,10 +54,25 @@ withApp perform = do pool :: H.Pool P.Postgres <- H.acquirePool pgSettings testPoolOpts + let txParam = (Just (H.ReadCommitted, Just True)) + + tblsRes <- H.session pool $ H.tx txParam alltables + let allTables = either (fail . show) id tblsRes + + relsRes <- H.session pool $ H.tx txParam allrelations + let allRelations = either (fail . show) id relsRes + + colsRes <- H.session pool $ H.tx txParam $ allcolumns allRelations + let allColumns = either (fail . show) id colsRes + + pkRes <- H.session pool $ H.tx txParam $ allprimaryKeys + let allPrimaryKeys = either (fail . show) id pkRes + + perform $ middle $ \req resp -> do body <- strictRequestBody req result <- liftIO $ H.session pool $ H.tx (Just (H.ReadCommitted, Just True)) - $ authenticated cfg (app cfg body) req + $ authenticated cfg (app allTables allRelations allColumns allPrimaryKeys cfg body) req either (resp . errResponse) resp result where middle = defaultMiddle False