integrated skin code gor generating Sql Query (only left to execute it)

This commit is contained in:
Ruslan Talpa
2015-09-23 14:31:24 +03:00
parent c0e17c44ba
commit fb92b76a1a
6 changed files with 410 additions and 29 deletions
+10
View File
@@ -52,6 +52,8 @@ executable postgrest
, mtl
, cassava
, jwt
, parsec
, errors
hs-source-dirs: src
library
@@ -88,8 +90,12 @@ library
, mtl
, cassava
, jwt
, parsec
, errors
Exposed-Modules: PostgREST.App
, PostgREST.Types
, PostgREST.Parsers
, PostgREST.Functions
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
@@ -111,6 +117,8 @@ Test-Suite spec
Main-Is: Main.hs
Other-Modules: PostgREST.App
, PostgREST.Types
, PostgREST.Parsers
, PostgREST.Functions
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
@@ -150,3 +158,5 @@ Test-Suite spec
, process
, heredoc
, jwt
, parsec
, errors
+17 -3
View File
@@ -9,7 +9,7 @@ module PostgREST.App (app, sqlError, isSqlError, contentTypeForAccept
import Control.Monad (join)
import Control.Arrow ((***), second)
import Control.Applicative
import Data.Bifunctor (first)
import Data.Text hiding (map, find, filter)
import Data.Maybe (fromMaybe, mapMaybe, isJust, isNothing)
import Text.Regex.TDFA ((=~))
@@ -47,6 +47,8 @@ import PostgREST.Auth
import PostgREST.PgQuery
import PostgREST.RangeQuery
import PostgREST.PgStructure
import PostgREST.Parsers
import PostgREST.Functions
import Prelude
@@ -66,20 +68,31 @@ app dbstructure conf reqBody role req =
([], _) -> do
let body = encode $ filter (filterTableAcl role) $ filter (((cs schema)==).tableSchema) allTables
return $ responseLBS status200 [jsonH, ("Custom", "header")] $ cs body
return $ responseLBS status200 [jsonH] $ cs body
([table], "OPTIONS") -> do
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
return $ responseLBS status200 [jsonH, allOrigins] $ cs body
([table], "GET") ->
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
else do
let apiRequest = parseGetRequest req
dbRequest = first formatParserError apiRequest
>>= traverse (requestNodeToQuery schema allTables allColumns)
>>= addRelations allRelations Nothing
>>= addJoinConditions allColumns
where formatParserError = pack.show
query = dbRequestToQuery <$> dbRequest
body = show query
return $ responseLBS status200 [] $ cs body
{--
let qt = qualify table
from = fromMaybe 0 $ rangeOffset <$> range
query = B.Stmt "select " V.empty True <>
@@ -110,6 +123,7 @@ app dbstructure conf reqBody role req =
if Prelude.null canonical then "" else "?" <> cs canonical
)
] (cs $ fromMaybe "[]" body)
--}
(["postgrest", "users"], "POST") -> do
let user = decode reqBody :: Maybe AuthUser
+168
View File
@@ -0,0 +1,168 @@
{-# LANGUAGE OverloadedStrings #-}
module PostgREST.Functions
where
import PostgREST.Types
import Control.Error
import Data.List (find)
import Data.Tree
import Data.Text hiding (find, foldr, map, null, last)
import Data.Monoid
import PostgREST.PgQuery (pgFmtOperator, pgFmtValue, pgFmtIdent, pgFmtLit, fromQi, QualifiedIdentifier(..))
findColumn :: [Column] -> Text -> Text -> Text -> Either Text Column
findColumn allColumns s t c = note ("no such column: "<>t<>"."<>c) $
find (\ col -> colSchema col == s && colTable col == t && colName col == c ) allColumns
findTable :: [Table] -> Text -> Text -> Either Text Table
findTable allTables s t = note ("no such table: "<>t) $
find (\tb-> s == tableSchema tb && t == tableName tb ) allTables
findRelation :: [Relation] -> Text -> Text -> Text -> Maybe Relation
findRelation allRelations s t1 t2 =
find (\r -> s == relSchema r && t1 == relTable r && t2 == relFTable r) allRelations
filterToCondition :: Text -> [Column] -> Text -> Filter -> Either Text Condition
filterToCondition schema allColumns table (Filter fld op val) =
Condition <$> c <*> pure op <*> pure (VText (pack val))
where
c = (,) <$> column <*> pure (snd fld)
column = findColumn allColumns schema table $ pack $ fst fld
requestNodeToQuery ::Text -> [Table] -> [Column] -> RequestNode -> Either Text Query
requestNodeToQuery schema allTables allColumns (RequestNode tblNameS flds fltrs) =
Select <$> mainTable <*> select <*> joinTables <*> qwhere <*> rel
where
tblName = pack tblNameS
mainTable = findTable allTables schema tblName
select = mapM toDbSelectItem flds --besides specific columns, we allow * here also
where
-- it's ok not to check that the table exists here, mainTable will do the checking
toDbSelectItem :: SelectItem -> Either Text DbSelectItem
toDbSelectItem (("*", Nothing), Nothing) = Right $ ((Star{colSchema = schema, colTable = tblName}, Nothing), Nothing)
toDbSelectItem ((c,jp), cast) = (,) <$> dbFld <*> pure cast
where
col = findColumn allColumns schema tblName $ pack c
dbFld = (,) <$> col <*> pure jp
qwhere = mapM (filterToCondition schema allColumns tblName) fltrs
joinTables = pure []
rel = pure Nothing
addRelations :: [Relation] -> Maybe DbRequest -> DbRequest -> Either Text DbRequest
addRelations allRelations parentNode node@(Node query@(Select {qMainTable=table}) forest) =
case parentNode of
Nothing -> Node query{qRelation=Nothing} <$> updatedForest
(Just (Node (Select{qMainTable=parentTable}) _)) -> Node <$> (addRel query <$> rel) <*> updatedForest
where
rel = note ("no relation between " <> (tableName table) <> " and " <> (tableName parentTable)) $
findRelation allRelations (tableSchema table) (tableName table) (tableName parentTable)
addRel :: Query -> Relation -> Query
addRel q r = q{qRelation = Just r}
where
updatedForest = mapM (addRelations allRelations (Just node)) forest
addJoinConditions :: [Column] -> Tree Query -> Either Text DbRequest
addJoinConditions allColumns (Node query@(Select{qRelation=relation}) forest) =
case relation of
Nothing -> Node <$> updatedQuery <*> updatedForest -- this is the root node
Just rel@(Relation{relType="child"}) -> Node <$> (addCond <$> updatedQuery <*> getJoinCondition rel) <*> updatedForest
Just (Relation{relType="parent"}) -> Node <$> updatedQuery <*> updatedForest
-- Just (Many relationColumn1 relationColumn2) -> Node <$> pure updatedQuery{qJoinTables=linkTable:qJoinTables updatedQuery, qWhere=cond1:cond2:qWhere updatedQuery} <*> updatedForest
-- where
-- cond1 = getJoinCondition relationColumn1
-- cond2 = getJoinCondition relationColumn2
-- linkTable = Table "public" (colTable relationColumn1) True
_ -> Left "unknow relation"
where
-- add parentTable and parentJoinConditions to the query
updatedQuery = foldr (flip addCond) (query{qJoinTables = parentTables ++ (qJoinTables query)}) <$> parentJoinConditions
where
parentJoinConditions = mapM (getJoinCondition.snd) parents
parentTables = map fst parents
parents = mapMaybe (getParents.rootLabel) forest
getParents qq@(Select{qRelation=(Just rel@(Relation{relType="parent"}))}) = Just (qMainTable qq, rel)
getParents _ = Nothing
updatedForest = mapM (addJoinConditions allColumns) forest
getJoinCondition rel@(Relation s t c _ _ _) = Condition <$> cc <*> pure "=" <*> pure (VForeignKey rel)
where
col = findColumn allColumns s t c
cc = (,) <$> col <*> pure Nothing
addCond q con = q{qWhere=con:qWhere q}
dbRequestToQuery :: DbRequest -> Text
dbRequestToQuery (Node (Select mainTable columns tables conditions relation) forest) =
case relation of
Nothing -> "SELECT "
<> "pg_catalog.count(t),"
<> "array_to_json(array_agg(row_to_json(t)))::CHARACTER VARYING AS json "
<> "FROM ("
<> query
<> ") t;"
_ -> query
where
query = Data.Text.unwords [
("WITH " <> intercalate ", " withs) `emptyOnNull` withs,
"SELECT ", intercalate ", " (map selectItemToStr columns ++ selects),
"FROM ", intercalate ", " (map pgFmtTable (mainTable:tables)),
("WHERE " <> intercalate " AND " ( map pgFmtCondition conditions )) `emptyOnNull` conditions
]
emptyOnNull val x = if null x then "" else val
(withs, selects) = foldr getQueryParts ([],[]) forest
--getQueryParts is not total but dbRequestToQuery is called only after addJoinConditions which ensures the only
--posible relations are Child Parent Many
getQueryParts :: Tree Query -> ([Text], [Text]) -> ([Text], [Text])
getQueryParts (Node q@(Select{qMainTable=table, qRelation=(Just (Relation {relType="child"}))}) forst) (w,s) = (w,sel:s)
where name = tableName table
sel = "("
<> "SELECT array_to_json(array_agg(row_to_json("<>name<>"))) "
<> "FROM (" <> dbRequestToQuery (Node q forst) <> ") " <> name
<> ") AS " <> name
getQueryParts (Node q@(Select{qMainTable=table, qRelation=(Just (Relation{relType="parent"}))}) forst) (w,s) = (wit:w,sel:s)
where name = tableName table
sel = "row_to_json(" <> name <> ".*) AS "<>name --TODO must be singular
wit = name <> " AS ( " <> dbRequestToQuery (Node q forst) <> " )"
-- getQueryParts (Node q@(Select{qMainTable=table, qRelation=(Just (Many _ _))}) forst) (w,s) = (w,sel:s)
-- where name = tableName table
-- sel = "("
-- <> "SELECT array_to_json(array_agg(row_to_json("<>name<>"))) "
-- <> "FROM (" <> dbRequestToQuery (Node q forst) <> ") " <> name
-- <> ") AS " <> name
-- the following is just to remove the warning, maybe relType should not be String?
getQueryParts (Node (Select{qRelation=Nothing}) _) _ = undefined
getQueryParts (Node (Select{qRelation=(Just (Relation {relType=_}))}) _) _ = undefined
pgFmtCondition :: Condition -> Text
pgFmtCondition (Condition (col,jp) ops val) = pgFmtColumn col <> pgFmtJsonPath jp <> opToStr op <> valToStr val
where
op = pack ops
opToStr o = pgFmtOperator o
valToStr v = case v of
VText s -> pgFmtValue op s
VForeignKey (Relation{relFTable=table, relFColumn=column}) -> table <> "." <> column
pgFmtColumn :: Column -> Text
pgFmtColumn Column {colSchema=s, colTable=t, colName=c} = pgFmtIdent s <> "." <> pgFmtIdent t <> "." <> pgFmtIdent c
pgFmtColumn Star {colSchema=s, colTable=t} = pgFmtIdent s <> "." <> pgFmtIdent t <> ".*"
pgFmtJsonPath :: Maybe JsonPath -> Text
pgFmtJsonPath (Just [x]) = "->>" <> (pgFmtLit $ pack x)
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit (pack x) <> pgFmtJsonPath ( Just xs )
pgFmtJsonPath _ = ""
pgFmtTable :: Table -> Text
pgFmtTable Table{tableSchema=s, tableName=n} = fromQi $ QualifiedIdentifier s n
selectItemToStr :: DbSelectItem -> Text
selectItemToStr ((c, jp), Nothing) = pgFmtColumn c <> pgFmtJsonPath jp <> asJsonPath jp
selectItemToStr ((c, jp), Just cast ) = "CAST (" <> pgFmtColumn c <> pgFmtJsonPath jp <> " AS " <> pack cast <> " )" <> asJsonPath jp
asJsonPath :: Maybe JsonPath -> Text
asJsonPath Nothing = ""
asJsonPath (Just xx) = " AS " <> (pack $ last xx)
+154
View File
@@ -0,0 +1,154 @@
--{-# LANGUAGE QuasiQuotes, ScopedTypeVariables, OverloadedStrings, FlexibleContexts #-}
module PostgREST.Parsers
-- ( parseGetRequest
-- , pSelect
-- , pField
-- , pRequestSelect
-- )
where
import Text.ParserCombinators.Parsec hiding (many, (<|>))
--import Text.Parsec.Text
--import Text.Parsec hiding (many, (<|>))
--import Text.Parsec.Prim hiding (many, (<|>))
import Control.Applicative
--import Control.Monad
--import qualified Data.Text as T
import Data.Tree
import Network.Wai (Request, pathInfo, queryString)
import PostgREST.Types
--import qualified Data.ByteString.Char8 as C
--import Control.Monad
--import Data.Foldable (foldrM)
import Data.List (delete, find)
import Data.Maybe
import Data.String.Conversions (cs)
--import qualified Data.ByteString.Char8 as C
--buildRequest :: String -> String -> [(String, String)] -> Either P.ParseError Request
parseGetRequest :: Request -> Either ParseError ApiRequest
parseGetRequest httpRequest =
foldr addFilter <$> apiRequest <*> flts
where
apiRequest = parse (pRequestSelect rootTableName) ("failed to parse select ("++selectStr++")") $ cs selectStr
flts = mapM pRequestFilter whereFilters
rootTableName = cs $ head $ pathInfo httpRequest -- TODO unsafe head
qString = [(cs k, cs <$> v)|(k,v) <- queryString httpRequest]
selectStr = fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qString --in case the parametre is missing or empty we default to *
whereFilters = [ (k, fromJust v) | (k,v) <- qString, k `notElem` ["select"], isJust v ]
pRequestSelect :: String -> Parser ApiRequest
pRequestSelect rootNodeName = do
fieldTree <- pFieldForest
return $ foldr treeEntry (Node (RequestNode rootNodeName [] []) []) fieldTree
where
treeEntry :: Tree SelectItem -> Tree RequestNode -> Tree RequestNode
treeEntry (Node fld@((fn, _),_) fldForest) (Node rNode rForest) =
case fldForest of
[] -> Node (rNode {fields=fld:fields rNode}) rForest
_ -> Node rNode (foldr treeEntry (Node (RequestNode fn [] []) []) fldForest:rForest)
pRequestFilter :: (String, String) -> Either ParseError (Path, Filter)
pRequestFilter (k, v) = (,) <$> path <*> (Filter <$> fld <*> op <*> val)
where
treePath = parse pTreePath ("failed to parser tree path ("++k++")") k
opVal = parse pOpValueExp ("failed to parse filter ("++v++")") v
path = fst <$> treePath
fld = snd <$> treePath
op = fst <$> opVal
val = snd <$> opVal
addFilter :: (Path, Filter) -> ApiRequest -> ApiRequest
addFilter ([], flt) (Node rn@(RequestNode {filters=flts}) forest) = Node (rn {filters=flt:flts}) forest
addFilter (path, flt) (Node rn forest) =
case targetNode of
Nothing -> Node rn forest -- the filter is silenty dropped in the Request does not contain the required path
Just tn -> Node rn (addFilter (remainingPath, flt) tn:restForest)
where
targetNodeName:remainingPath = path
(targetNode,restForest) = splitForest targetNodeName forest
splitForest name forst =
case maybeNode of
Nothing -> (Nothing,forest)
Just node -> (Just node, delete node forest)
where maybeNode = find ((name==).nodeName.rootLabel) forst
ws :: Parser String
ws = many (oneOf " \t")
--lexeme :: Parser String -> Parser String
--lexeme :: Text.Parsec.Prim.ParsecT String () Data.Functor.Identity.Identity a -> Text.Parsec.Prim.ParsecT String () Data.Functor.Identity.Identity a
--lexeme :: Text.Parsec.Prim.ParsecT String () Data.Functor.Identity.Identity Char -> Text.Parsec.Prim.ParsecT String () Data.Functor.Identity.Identity Char
lexeme p = ws *> p <* ws
pTreePath :: Parser (Path,Field)
pTreePath = do
p <- (pFieldName `sepBy1` pDelimiter)
--f <- pField
jp <- optionMaybe ( string "->" >> pJsonPath)
return (init p, (last p, jp))
pFieldForest :: Parser [Tree SelectItem]
pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
pFieldTree :: Parser (Tree SelectItem)
pFieldTree =
try ( do
fld <- pSelect
char '('
subforest <- pFieldForest
char ')'
return (Node fld subforest)
)
<|> do
fld <- pSelect
return (Node fld [])
pStar :: Parser String
pStar = string "*" *> pure "*"
pFieldName :: Parser String
pFieldName = many1 (letter <|> digit <|> oneOf "_")
<?> "field name (* or [a..z0..9_])"
pJsonPath :: Parser [String]
pJsonPath = pFieldName `sepBy1` (try (string "->>") <|> string "->")
pField :: Parser Field
pField = lexeme $ do
f <- pFieldName
jp <- optionMaybe ( (try (string "->>") <|> string "->") >> pJsonPath)
return (f, jp)
pSelect :: Parser SelectItem
pSelect = lexeme $
try (do
n <- pField
v <- optionMaybe (string "::" >> many letter)
return (n, v)
)
<|> do
s <- pStar
return ((s, Nothing), Nothing)
pOperator :: Parser Operator
pOperator = try (string "eq")
<|> try (string "gt")
<|> try (string "lt")
<?> "operator (eq, gt, ...)"
pInt :: Parser Int
pInt = try (liftA read (many1 digit)) <?> "integer"
--pValue :: Parser Value
--pValue = (VInt <$> try (pInt <* eof))
-- <|>(VString <$> many anyChar)
pValue :: Parser FValue
pValue = many anyChar
pDelimiter :: Parser Char
pDelimiter = char '.' <?> "delimiter (.)"
pOpValueExp :: Parser (Operator, FValue)
pOpValueExp = liftA2 (,) pOperator (pDelimiter *> pValue)
+36 -25
View File
@@ -222,35 +222,46 @@ wherePred table (col, predicate) =
opCode = hasNot (head rest) headPredicate
notOp = hasNot headPredicate ""
value = hasNot (T.intercalate "." $ tail rest) (T.intercalate "." rest)
whiteList val = fromMaybe
(cs (pgFmtLit val) <> "::unknown ")
(L.find ((==) . T.toLower $ val) ["null","true","false"])
sqlValue = pgFmtValue opCode value
op = pgFmtOperator opCode
whiteList :: T.Text -> T.Text
whiteList val = fromMaybe
(cs (pgFmtLit val) <> "::unknown ")
(L.find ((==) . T.toLower $ val) ["null","true","false"])
pgFmtValue :: T.Text -> T.Text -> T.Text
pgFmtValue opCode value =
case opCode of
"like" -> unknownLiteral $ T.map star value
"ilike" -> unknownLiteral $ T.map star value
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"notin" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"@@" -> "to_tsquery(" <> unknownLiteral value <> ") "
_ -> unknownLiteral value
where
star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit
sqlValue = case opCode of
"like" -> unknownLiteral $ T.map star value
"ilike" -> unknownLiteral $ T.map star value
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"notin" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"@@" -> "to_tsquery(" <> unknownLiteral value <> ") "
_ -> unknownLiteral value
pgFmtOperator :: T.Text -> T.Text
pgFmtOperator opCode =
case opCode of
"eq" -> "="
"gt" -> ">"
"lt" -> "<"
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
"like"-> "like"
"ilike"-> "ilike"
"in" -> "in"
"notin" -> "not in"
"is" -> "is"
"isnot" -> "is not"
"@@" -> "@@"
_ -> "="
op = case opCode of
"eq" -> "="
"gt" -> ">"
"lt" -> "<"
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
"like"-> "like"
"ilike"-> "ilike"
"in" -> "in"
"notin" -> "not in"
"is" -> "is"
"isnot" -> "is not"
"@@" -> "@@"
_ -> "="
orderParse :: Net.Query -> [OrderTerm]
orderParse q =
+25 -1
View File
@@ -1,5 +1,6 @@
module PostgREST.Types where
import Data.Text
import Data.Tree
data DbStructure = DbStructure {
tables :: [Table]
@@ -34,7 +35,7 @@ data Column = Column {
, colDefault :: Maybe Text
, colEnum :: [Text]
, colFK :: Maybe ForeignKey
} deriving (Show)
} | Star {colSchema :: Text, colTable :: Text } deriving (Show)
data PrimaryKey = PrimaryKey {
pkSchema::Text, pkTable::Text, pkName::Text
@@ -48,3 +49,26 @@ data Relation = Relation {
, relFColumn :: Text
, relType :: Text
} deriving (Show, Eq)
--------
-- Request Types
type Operator = String
type FValue = String
type ApiRequest = Tree RequestNode
type FieldName = String
type JsonPath = [String]
type Field = (FieldName, Maybe JsonPath)
type Cast = String
type SelectItem = (Field, Maybe Cast)
type Path = [String]
data RequestNode = RequestNode {nodeName::String, fields::[SelectItem], filters::[Filter]} deriving (Show, Eq)
data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq)
-- Db Request Types
type DbField = (Column, Maybe JsonPath)
type DbSelectItem = (DbField, Maybe Cast)
data DbValue = VText Text | VForeignKey Relation deriving (Show)
data Condition = Condition {conColumn::DbField, conOperator::Operator, conValue::DbValue} deriving (Show)
data Query = Select {qMainTable::Table, qSelect::[DbSelectItem], qJoinTables::[Table], qWhere::[Condition], qRelation::Maybe Relation} deriving (Show)
type DbRequest = Tree Query