moved db structure detection at the beginning (2 tests failing)

This commit is contained in:
Ruslan Talpa
2015-09-22 16:56:19 +03:00
parent 6bd6108619
commit a7b883c922
7 changed files with 349 additions and 129 deletions
+4 -1
View File
@@ -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
+33 -12
View File
@@ -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
+3 -3
View File
@@ -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)
+27 -2
View File
@@ -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
+224 -110
View File
@@ -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
+41
View File
@@ -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)
+17 -1
View File
@@ -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