Merge pull request #712 from ruslantalpa/merge_upstream
fix relation detection bug and allow offset on lower levels
This commit is contained in:
@@ -14,9 +14,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Implement support for singular representation responses for POST/PATCH requests - @ehamberg
|
- Implement support for singular representation responses for POST/PATCH requests - @ehamberg
|
||||||
- Include RPC endpoints in OpenAPI output - @begriffs, @LogvinovLeon
|
- Include RPC endpoints in OpenAPI output - @begriffs, @LogvinovLeon
|
||||||
- Custom request validation with `--pre-request` argument - @begriffs
|
- Custom request validation with `--pre-request` argument - @begriffs
|
||||||
|
- Ability to specify offset for a deeper level - @ruslantalpa
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Do not apply limit to parent items - @ruslantalpa
|
- Do not apply limit to parent items - @ruslantalpa
|
||||||
|
- Fix bug in relation detection when selecting parents two levels up by using the name of the FK - @ruslantalpa
|
||||||
- Customize content negotiation per route - @begriffs
|
- Customize content negotiation per route - @begriffs
|
||||||
- Allow using nulls order without explicit order direction - @steve-chavez
|
- Allow using nulls order without explicit order direction - @steve-chavez
|
||||||
|
|
||||||
|
|||||||
+18
-10
@@ -20,11 +20,12 @@ import Network.HTTP.Types.Header (hAuthorization, hContentType, Header
|
|||||||
import Network.HTTP.Types.URI (parseSimpleQuery)
|
import Network.HTTP.Types.URI (parseSimpleQuery)
|
||||||
import Network.Wai (Request (..))
|
import Network.Wai (Request (..))
|
||||||
import Network.Wai.Parse (parseHttpAccept)
|
import Network.Wai.Parse (parseHttpAccept)
|
||||||
import PostgREST.RangeQuery (NonnegRange, rangeRequested, restrictRange, rangeGeq, allRange)
|
import PostgREST.RangeQuery (NonnegRange, rangeRequested, restrictRange, rangeGeq, allRange, rangeLimit, rangeOffset)
|
||||||
|
import Data.Ranged.Boundaries
|
||||||
import PostgREST.Types (QualifiedIdentifier (..),
|
import PostgREST.Types (QualifiedIdentifier (..),
|
||||||
Schema, Payload(..),
|
Schema, Payload(..),
|
||||||
UniformObjects(..))
|
UniformObjects(..))
|
||||||
import Data.Ranged.Ranges (singletonRange, rangeIntersection)
|
import Data.Ranged.Ranges (Range(..), singletonRange, rangeIntersection)
|
||||||
|
|
||||||
type RequestBody = BL.ByteString
|
type RequestBody = BL.ByteString
|
||||||
|
|
||||||
@@ -146,15 +147,14 @@ userApiRequest schema req reqBody =
|
|||||||
ApiRequest {
|
ApiRequest {
|
||||||
iAction = action
|
iAction = action
|
||||||
, iTarget = target
|
, iTarget = target
|
||||||
, iRange = M.insert "limit" (rangeIntersection headerRange urlRange) $
|
, iRange = ranges
|
||||||
M.fromList [ (toS k, restrictRange (readBSMaybe =<< v) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k ]
|
|
||||||
, iAccepts = fromMaybe [CTAny] $
|
, iAccepts = fromMaybe [CTAny] $
|
||||||
map decodeContentType . parseHttpAccept <$> lookupHeader "accept"
|
map decodeContentType . parseHttpAccept <$> lookupHeader "accept"
|
||||||
, iPayload = relevantPayload
|
, iPayload = relevantPayload
|
||||||
, iPreferRepresentation = representation
|
, iPreferRepresentation = representation
|
||||||
, iPreferSingular = singular
|
, iPreferSingular = singular
|
||||||
, iPreferCount = not singular && hasPrefer "count=exact"
|
, iPreferCount = not singular && hasPrefer "count=exact"
|
||||||
, iFilters = [ (toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, k /= "select", k /= "offset", not (endingIn ["order", "limit"] k) ]
|
, iFilters = [ (toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, k /= "select", not (endingIn ["order", "limit", "offset"] k) ]
|
||||||
, iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
, iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
||||||
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
||||||
, iCanonicalQS = toS $ urlEncodeVars
|
, iCanonicalQS = toS $ urlEncodeVars
|
||||||
@@ -191,11 +191,19 @@ userApiRequest schema req reqBody =
|
|||||||
where lastWord = last $ T.split (=='.') key
|
where lastWord = last $ T.split (=='.') key
|
||||||
|
|
||||||
headerRange = if singular && method == "GET" then singletonRange 0 else rangeRequested hdrs
|
headerRange = if singular && method == "GET" then singletonRange 0 else rangeRequested hdrs
|
||||||
urlOffsetRange = rangeGeq . fromMaybe (0::Integer) $
|
replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) ++ [x]
|
||||||
readBSMaybe =<< join (lookup "offset" qParams)
|
limitParams :: M.HashMap ByteString NonnegRange
|
||||||
urlRange = restrictRange
|
limitParams = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe =<< (toS <$> v)) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]
|
||||||
(readBSMaybe =<< join (lookup "limit" qParams))
|
offsetParams :: M.HashMap ByteString NonnegRange
|
||||||
urlOffsetRange
|
offsetParams = M.fromList [(toS (replaceLast "limit" k), fromMaybe allRange (rangeGeq <$> (readMaybe =<< (toS <$> v)))) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
|
||||||
|
|
||||||
|
urlRange = M.unionWith f limitParams offsetParams
|
||||||
|
where
|
||||||
|
f rl ro = Range (BoundaryBelow o) (BoundaryAbove $ o + l - 1)
|
||||||
|
where
|
||||||
|
l = fromMaybe 0 $ rangeLimit rl
|
||||||
|
o = rangeOffset ro
|
||||||
|
ranges = M.insert "limit" (rangeIntersection headerRange (fromMaybe allRange (M.lookup "limit" urlRange))) urlRange
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Find the best match from a list of content types accepted by the
|
Find the best match from a list of content types accepted by the
|
||||||
|
|||||||
@@ -558,73 +558,73 @@ allSynonyms cols =
|
|||||||
where
|
where
|
||||||
-- query explanation at https://gist.github.com/ruslantalpa/2eab8c930a65e8043d8f
|
-- query explanation at https://gist.github.com/ruslantalpa/2eab8c930a65e8043d8f
|
||||||
sql = [q|
|
sql = [q|
|
||||||
WITH view_columns AS (
|
with view_columns as (
|
||||||
SELECT
|
select
|
||||||
c.oid AS view_oid,
|
c.oid as view_oid,
|
||||||
a.attname::information_schema.sql_identifier AS column_name
|
a.attname::information_schema.sql_identifier as column_name
|
||||||
FROM pg_attribute a
|
from pg_attribute a
|
||||||
JOIN pg_class c ON a.attrelid = c.oid
|
join pg_class c on a.attrelid = c.oid
|
||||||
JOIN pg_namespace nc ON c.relnamespace = nc.oid
|
join pg_namespace nc on c.relnamespace = nc.oid
|
||||||
WHERE
|
where
|
||||||
NOT pg_is_other_temp_schema(nc.oid)
|
not pg_is_other_temp_schema(nc.oid)
|
||||||
AND a.attnum > 0
|
and a.attnum > 0
|
||||||
AND NOT a.attisdropped
|
and not a.attisdropped
|
||||||
AND (c.relkind = 'v'::"char")
|
and (c.relkind = 'v'::"char")
|
||||||
AND nc.nspname NOT IN ('information_schema', 'pg_catalog')
|
and nc.nspname not in ('information_schema', 'pg_catalog')
|
||||||
),
|
),
|
||||||
view_column_usage AS (
|
view_column_usage as (
|
||||||
SELECT DISTINCT
|
select distinct
|
||||||
v.oid as view_oid,
|
v.oid as view_oid,
|
||||||
nv.nspname::information_schema.sql_identifier AS view_schema,
|
nv.nspname::information_schema.sql_identifier as view_schema,
|
||||||
v.relname::information_schema.sql_identifier AS view_name,
|
v.relname::information_schema.sql_identifier as view_name,
|
||||||
nt.nspname::information_schema.sql_identifier AS table_schema,
|
nt.nspname::information_schema.sql_identifier as table_schema,
|
||||||
t.relname::information_schema.sql_identifier AS table_name,
|
t.relname::information_schema.sql_identifier as table_name,
|
||||||
a.attname::information_schema.sql_identifier AS column_name,
|
a.attname::information_schema.sql_identifier as column_name,
|
||||||
pg_get_viewdef(v.oid)::information_schema.character_data AS view_definition
|
pg_get_viewdef(v.oid)::information_schema.character_data as view_definition
|
||||||
FROM pg_namespace nv
|
from pg_namespace nv
|
||||||
JOIN pg_class v ON nv.oid = v.relnamespace
|
join pg_class v on nv.oid = v.relnamespace
|
||||||
JOIN pg_depend dv ON v.oid = dv.refobjid
|
join pg_depend dv on v.oid = dv.refobjid
|
||||||
JOIN pg_depend dt ON dv.objid = dt.objid
|
join pg_depend dt on dv.objid = dt.objid
|
||||||
JOIN pg_class t ON dt.refobjid = t.oid
|
join pg_class t on dt.refobjid = t.oid
|
||||||
JOIN pg_namespace nt ON t.relnamespace = nt.oid
|
join pg_namespace nt on t.relnamespace = nt.oid
|
||||||
JOIN pg_attribute a ON t.oid = a.attrelid AND dt.refobjsubid = a.attnum
|
join pg_attribute a on t.oid = a.attrelid and dt.refobjsubid = a.attnum
|
||||||
|
|
||||||
WHERE
|
where
|
||||||
nv.nspname not in ('information_schema', 'pg_catalog')
|
nv.nspname not in ('information_schema', 'pg_catalog')
|
||||||
AND v.relkind = 'v'::"char"
|
and v.relkind = 'v'::"char"
|
||||||
AND dv.refclassid = 'pg_class'::regclass::oid
|
and dv.refclassid = 'pg_class'::regclass::oid
|
||||||
AND dv.classid = 'pg_rewrite'::regclass::oid
|
and dv.classid = 'pg_rewrite'::regclass::oid
|
||||||
AND dv.deptype = 'i'::"char"
|
and dv.deptype = 'i'::"char"
|
||||||
AND dv.refobjid <> dt.refobjid
|
and dv.refobjid <> dt.refobjid
|
||||||
AND dt.classid = 'pg_rewrite'::regclass::oid
|
and dt.classid = 'pg_rewrite'::regclass::oid
|
||||||
AND dt.refclassid = 'pg_class'::regclass::oid
|
and dt.refclassid = 'pg_class'::regclass::oid
|
||||||
AND (t.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
|
and (t.relkind = any (array['r'::"char", 'v'::"char", 'f'::"char"]))
|
||||||
),
|
),
|
||||||
candidates AS (
|
candidates as (
|
||||||
SELECT
|
select
|
||||||
vcu.*,
|
vcu.*,
|
||||||
(
|
(
|
||||||
SELECT CASE WHEN match IS NOT NULL THEN coalesce(match[7], match[4]) END
|
select case when match is not null then coalesce(match[8], match[7], match[4]) end
|
||||||
FROM REGEXP_MATCHES(
|
from regexp_matches(
|
||||||
CONCAT('SELECT ', SPLIT_PART(vcu.view_definition, 'SELECT', 2)),
|
CONCAT('SELECT ', SPLIT_PART(vcu.view_definition, 'SELECT', 2)),
|
||||||
CONCAT('SELECT.*?((',vcu.table_name,')|(\w+))\.(', vcu.column_name, ')(\sAS\s(")?([^"]+)\6)?.*?FROM.*?',vcu.table_schema,'\.(\2|',vcu.table_name,'\s+(AS\s)?\3)'),
|
CONCAT('SELECT.*?((',vcu.table_name,')|(\w+))\.(', vcu.column_name, ')(\s+AS\s+("([^"]+)"|([^, \n\t]+)))?.*?FROM.*?',vcu.table_schema,'\.(\2|',vcu.table_name,'\s+(as\s)?\3)'),
|
||||||
'ns'
|
'nsi'
|
||||||
) match
|
) match
|
||||||
) AS view_column_name
|
) as view_column_name
|
||||||
FROM view_column_usage AS vcu
|
from view_column_usage as vcu
|
||||||
)
|
)
|
||||||
SELECT
|
select
|
||||||
c.table_schema,
|
c.table_schema,
|
||||||
c.table_name,
|
c.table_name,
|
||||||
c.column_name AS table_column_name,
|
c.column_name as table_column_name,
|
||||||
c.view_schema,
|
c.view_schema,
|
||||||
c.view_name,
|
c.view_name,
|
||||||
c.view_column_name
|
c.view_column_name
|
||||||
FROM view_columns AS vc, candidates AS c
|
from view_columns as vc, candidates as c
|
||||||
WHERE
|
where
|
||||||
vc.view_oid = c.view_oid AND
|
vc.view_oid = c.view_oid
|
||||||
vc.column_name = c.view_column_name
|
and vc.column_name = c.view_column_name
|
||||||
ORDER BY c.view_schema, c.view_name, c.table_name, c.view_column_name
|
order by c.view_schema, c.view_name, c.table_name, c.view_column_name
|
||||||
|]
|
|]
|
||||||
|
|
||||||
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column)
|
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import qualified Hasql.Decoders as HD
|
|||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset, allRange)
|
import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset, allRange)
|
||||||
import Control.Error (note)
|
import Control.Error (note, hush)
|
||||||
import Data.Functor.Contravariant (contramap)
|
import Data.Functor.Contravariant (contramap)
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import Data.Text (intercalate, unwords, replace, isInfixOf, toLower, split)
|
import Data.Text (intercalate, unwords, replace, isInfixOf, toLower, split)
|
||||||
@@ -53,6 +53,7 @@ import Data.Scientific ( FPFormat (..)
|
|||||||
, isInteger
|
, isInteger
|
||||||
)
|
)
|
||||||
import Protolude hiding (from, intercalate, ord, cast)
|
import Protolude hiding (from, intercalate, ord, cast)
|
||||||
|
import Unsafe (unsafeHead)
|
||||||
import PostgREST.ApiRequest (PreferRepresentation (..))
|
import PostgREST.ApiRequest (PreferRepresentation (..))
|
||||||
|
|
||||||
{-| The generic query result format used by API responses. The location header
|
{-| The generic query result format used by API responses. The location header
|
||||||
@@ -157,33 +158,74 @@ createWriteStatement qi selectQuery mutateQuery isSingle Full
|
|||||||
| otherwise = asJsonF
|
| otherwise = asJsonF
|
||||||
|
|
||||||
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest
|
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest
|
||||||
addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _, alias)) forest) =
|
addRelations schema allRelations parentNode (Node readNode@(query, (name, _, alias)) forest) =
|
||||||
case parentNode of
|
case parentNode of
|
||||||
(Just (Node (Select{from=[parentTable]}, (_, _, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest
|
(Just (Node (Select{from=[parentNodeTable]}, (_, _, _)) _)) ->
|
||||||
|
Node <$> readNode' <*> forest'
|
||||||
where
|
where
|
||||||
rel = note ("no relation between " <> parentTable <> " and " <> name)
|
forest' = updateForest $ hush node'
|
||||||
$ findRelationByTable schema name parentTable
|
node' = Node <$> readNode' <*> pure forest
|
||||||
<|> findRelationByColumn schema parentTable name
|
readNode' = addRel readNode <$> rel
|
||||||
|
rel :: Either Text Relation
|
||||||
|
rel = note ("no relation between " <> parentNodeTable <> " and " <> name)
|
||||||
|
$ findRelation schema name parentNodeTable
|
||||||
|
|
||||||
|
where
|
||||||
|
findRelation s nodeTableName parentNodeTableName =
|
||||||
|
find (\r ->
|
||||||
|
s == tableSchema (relTable r) && -- match schema for relation table
|
||||||
|
s == tableSchema (relFTable r) && -- match schema for relation foriegn table
|
||||||
|
(
|
||||||
|
|
||||||
|
-- (request) => projects { ..., clients{...} }
|
||||||
|
-- will match
|
||||||
|
-- (relation type) => parent
|
||||||
|
-- (entity) => clients {id}
|
||||||
|
-- (foriegn entity) => projects {client_id}
|
||||||
|
(
|
||||||
|
nodeTableName == tableName (relTable r) && -- match relation table name
|
||||||
|
parentNodeTableName == tableName (relFTable r) -- match relation foreign table name
|
||||||
|
) ||
|
||||||
|
|
||||||
|
|
||||||
|
-- (request) => projects { ..., client_id{...} }
|
||||||
|
-- will match
|
||||||
|
-- (relation type) => parent
|
||||||
|
-- (entity) => clients {id}
|
||||||
|
-- (foriegn entity) => projects {client_id}
|
||||||
|
(
|
||||||
|
parentNodeTableName == tableName (relFTable r) &&
|
||||||
|
length (relFColumns r) == 1 &&
|
||||||
|
nodeTableName `colMatches` (colName . unsafeHead . relFColumns) r
|
||||||
|
)
|
||||||
|
|
||||||
|
-- (request) => project_id { ..., client_id{...} }
|
||||||
|
-- will match
|
||||||
|
-- (relation type) => parent
|
||||||
|
-- (entity) => clients {id}
|
||||||
|
-- (foriegn entity) => projects {client_id}
|
||||||
|
-- this case works becasue before reaching this place
|
||||||
|
-- addRelation will turn project_id to project so the above condition will match
|
||||||
|
)
|
||||||
|
) allRelations
|
||||||
|
where n `colMatches` rc = (toS ("^" <> rc <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (toS n :: BS.ByteString)
|
||||||
addRel :: (ReadQuery, (NodeName, Maybe Relation, Maybe Alias)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation, Maybe Alias))
|
addRel :: (ReadQuery, (NodeName, Maybe Relation, Maybe Alias)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation, Maybe Alias))
|
||||||
addRel (query', (n, _, a)) r = (query' {from=fromRelation}, (n, Just r, a))
|
addRel (query', (n, _, a)) r = (query' {from=fromRelation}, (n, Just r, a))
|
||||||
where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from query')
|
where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from query')
|
||||||
|
|
||||||
_ -> Node (query, (name, Nothing, alias)) <$> updatedForest
|
_ -> n' <$> updateForest (Just (n' forest))
|
||||||
|
where
|
||||||
|
n' = Node (query, (name, Just r, alias))
|
||||||
|
t = Table schema name True -- !!! TODO find another way to get the table from the query
|
||||||
|
r = Relation t [] t [] Root Nothing Nothing Nothing
|
||||||
where
|
where
|
||||||
updatedForest = mapM (addRelations schema allRelations (Just node)) forest
|
updateForest :: Maybe ReadRequest -> Either Text [ReadRequest]
|
||||||
-- Searches through all the relations and returns a match given the parameter conditions.
|
updateForest n = mapM (addRelations schema allRelations n) forest
|
||||||
-- Will only find a relation where both schemas are in the PostgREST schema.
|
|
||||||
-- `findRelationByColumn` also does a ducktype check to see if the column name has any variation of `id` or `fk`. If so then the relation is returned as a match.
|
|
||||||
findRelationByTable s t1 t2 =
|
|
||||||
find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t1 == tableName (relTable r) && t2 == tableName (relFTable r)) allRelations
|
|
||||||
findRelationByColumn s t c =
|
|
||||||
find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t == tableName (relFTable r) && length (relFColumns r) == 1 && c `colMatches` fromMaybe "" (colName <$> (head . relFColumns) r)) allRelations
|
|
||||||
where n `colMatches` rc = (toS ("^" <> rc <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (toS n :: BS.ByteString)
|
|
||||||
|
|
||||||
addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest
|
addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest
|
||||||
addJoinConditions schema (Node nn@(query, (n, r, a)) forest) =
|
addJoinConditions schema (Node nn@(query, (n, r, a)) forest) =
|
||||||
case r of
|
case r of
|
||||||
Nothing -> Node nn <$> updatedForest -- this is the root node
|
Just Relation{relType=Root} -> Node nn <$> updatedForest -- this is the root node
|
||||||
Just rel@Relation{relType=Child} -> Node (addCond query (getJoinConditions rel),(n,r,a)) <$> updatedForest
|
Just rel@Relation{relType=Child} -> Node (addCond query (getJoinConditions rel),(n,r,a)) <$> updatedForest
|
||||||
Just Relation{relType=Parent} -> Node nn <$> updatedForest
|
Just Relation{relType=Parent} -> Node nn <$> updatedForest
|
||||||
Just rel@Relation{relType=Many, relLTable=(Just linkTable)} ->
|
Just rel@Relation{relType=Many, relLTable=(Just linkTable)} ->
|
||||||
@@ -284,7 +326,7 @@ requestToQuery _ _ (DbMutate (Update _ (PayloadParseError _) _)) = undefined
|
|||||||
requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions ord range, (nodeName, maybeRelation, _)) forest)) =
|
requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions ord range, (nodeName, maybeRelation, _)) forest)) =
|
||||||
query
|
query
|
||||||
where
|
where
|
||||||
-- TODO! the folloing helper functions are just to remove the "schema" part when the table is "source" which is the name
|
-- TODO! the following helper functions are just to remove the "schema" part when the table is "source" which is the name
|
||||||
-- of our WITH query part
|
-- of our WITH query part
|
||||||
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
|
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
|
||||||
tblSchema tbl = if tbl == sourceCTEName then "" else schema
|
tblSchema tbl = if tbl == sourceCTEName then "" else schema
|
||||||
@@ -319,6 +361,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions
|
|||||||
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
|
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
|
||||||
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
|
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
|
||||||
where subquery = requestToQuery schema False (DbRead (Node n forst))
|
where subquery = requestToQuery schema False (DbRead (Node n forst))
|
||||||
|
|
||||||
getQueryParts (Node n@(_, (name, Just r@Relation{relType=Parent,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (joi:j,sel:s)
|
getQueryParts (Node n@(_, (name, Just r@Relation{relType=Parent,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (joi:j,sel:s)
|
||||||
where
|
where
|
||||||
node_name = fromMaybe name alias
|
node_name = fromMaybe name alias
|
||||||
@@ -339,7 +382,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions
|
|||||||
--the following is just to remove the warning
|
--the following is just to remove the warning
|
||||||
--getQueryParts is not total but requestToQuery is called only after addJoinConditions which ensures the only
|
--getQueryParts is not total but requestToQuery is called only after addJoinConditions which ensures the only
|
||||||
--posible relations are Child Parent Many
|
--posible relations are Child Parent Many
|
||||||
getQueryParts (Node (_,(_,Nothing,_)) _) _ = undefined
|
getQueryParts _ _ = undefined --error "undefined getQueryParts"
|
||||||
requestToQuery schema _ (DbMutate (Insert mainTbl (PayloadJSON (UniformObjects rows)))) =
|
requestToQuery schema _ (DbMutate (Insert mainTbl (PayloadJSON (UniformObjects rows)))) =
|
||||||
let qi = QualifiedIdentifier schema mainTbl
|
let qi = QualifiedIdentifier schema mainTbl
|
||||||
cols = map pgFmtIdent $ fromMaybe [] (HM.keys <$> (rows V.!? 0))
|
cols = map pgFmtIdent $ fromMaybe [] (HM.keys <$> (rows V.!? 0))
|
||||||
@@ -437,6 +480,7 @@ getJoinConditions (Relation t cols ft fcs typ lt lc1 lc2) =
|
|||||||
Child -> zipWith (toFilter tN ftN) cols fcs
|
Child -> zipWith (toFilter tN ftN) cols fcs
|
||||||
Parent -> zipWith (toFilter tN ftN) cols fcs
|
Parent -> zipWith (toFilter tN ftN) cols fcs
|
||||||
Many -> zipWith (toFilter tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toFilter ftN ltN) fcs (fromMaybe [] lc2)
|
Many -> zipWith (toFilter tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toFilter ftN ltN) fcs (fromMaybe [] lc2)
|
||||||
|
Root -> undefined --error "undefined getJoinConditions"
|
||||||
where
|
where
|
||||||
s = if typ == Parent then "" else tableSchema t
|
s = if typ == Parent then "" else tableSchema t
|
||||||
tN = tableName t
|
tN = tableName t
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ data QualifiedIdentifier = QualifiedIdentifier {
|
|||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
|
||||||
data RelationType = Child | Parent | Many deriving (Show, Eq)
|
data RelationType = Child | Parent | Many | Root deriving (Show, Eq)
|
||||||
data Relation = Relation {
|
data Relation = Relation {
|
||||||
relTable :: Table
|
relTable :: Table
|
||||||
, relColumns :: [Column]
|
, relColumns :: [Column]
|
||||||
|
|||||||
@@ -229,6 +229,14 @@ spec = do
|
|||||||
get "/projects?id=eq.1&select=myId:id, name, project_client:client_id{*}, project_tasks:tasks{id, name}" `shouldRespondWith`
|
get "/projects?id=eq.1&select=myId:id, name, project_client:client_id{*}, project_tasks:tasks{id, name}" `shouldRespondWith`
|
||||||
[str|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
[str|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||||
|
|
||||||
|
it "requesting parents two levels up while using FK to specify the link" $
|
||||||
|
get "/tasks?id=eq.1&select=id,name,project:project_id{id,name,client:client_id{id,name}}" `shouldRespondWith`
|
||||||
|
[str|[{"id":1,"name":"Design w7","project":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]
|
||||||
|
|
||||||
|
it "requesting parents two levels up while using FK to specify the link (with rename)" $
|
||||||
|
get "/tasks?id=eq.1&select=id,name,project:project_id{id,name,client:client_id{id,name}}" `shouldRespondWith`
|
||||||
|
[str|[{"id":1,"name":"Design w7","project":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]
|
||||||
|
|
||||||
|
|
||||||
it "requesting parents and filtering parent columns" $
|
it "requesting parents and filtering parent columns" $
|
||||||
get "/projects?id=eq.1&select=id, name, clients{id}" `shouldRespondWith`
|
get "/projects?id=eq.1&select=id, name, clients{id}" `shouldRespondWith`
|
||||||
@@ -263,6 +271,11 @@ spec = do
|
|||||||
get "/projects_view?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith`
|
get "/projects_view?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith`
|
||||||
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||||
|
|
||||||
|
it "requesting parents and children on views with renamed keys" $
|
||||||
|
get "/projects_view_alt?t_id=eq.1&select=t_id, name, clients{*}, tasks{id, name}" `shouldRespondWith`
|
||||||
|
[str|[{"t_id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||||
|
|
||||||
|
|
||||||
it "requesting children with composite key" $
|
it "requesting children with composite key" $
|
||||||
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments{content}" `shouldRespondWith`
|
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments{content}" `shouldRespondWith`
|
||||||
[str|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
|
[str|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
|
||||||
|
|||||||
@@ -167,16 +167,13 @@ spec = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "limit works on all levels" $
|
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=1&projects.tasks.order=id.asc&projects.tasks.limit=2"
|
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` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]}]}]|]
|
matchBody = Just [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1}]},{"id":2,"tasks":[{"id":3}]}]}]|]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "fails on offset specified below level 1" $
|
|
||||||
get "/clients?select=id,projects{id,tasks{id}}&projects.offset=2&projects.limit=1"
|
|
||||||
`shouldRespondWith` 400
|
|
||||||
|
|
||||||
it "limit and offset works on first level" $
|
it "limit and offset works on first level" $
|
||||||
get "/items?select=id&order=id.asc&limit=3&offset=2"
|
get "/items?select=id&order=id.asc&limit=3&offset=2"
|
||||||
|
|||||||
Vendored
+1
@@ -29,6 +29,7 @@ GRANT ALL ON TABLE
|
|||||||
, nullable_integer
|
, nullable_integer
|
||||||
, projects
|
, projects
|
||||||
, projects_view
|
, projects_view
|
||||||
|
, projects_view_alt
|
||||||
, simple_pk
|
, simple_pk
|
||||||
, tasks
|
, tasks
|
||||||
, filtered_tasks
|
, filtered_tasks
|
||||||
|
|||||||
Vendored
+6
@@ -654,6 +654,12 @@ CREATE VIEW projects_view AS
|
|||||||
FROM projects;
|
FROM projects;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE VIEW projects_view_alt AS
|
||||||
|
SELECT projects.id as t_id,
|
||||||
|
projects.name,
|
||||||
|
projects.client_id as t_client_id
|
||||||
|
FROM projects;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: simple_pk; Type: TABLE; Schema: test; Owner: -
|
-- Name: simple_pk; Type: TABLE; Schema: test; Owner: -
|
||||||
--
|
--
|
||||||
|
|||||||
Reference in New Issue
Block a user