Add Preference-Applied header for POST upsert
- Ensure creating nothing on ignore-duplicates succeeds - Refactor locationF query
This commit is contained in:
committed by
Steve Chávez
parent
6675821c64
commit
108f3cd651
@@ -116,8 +116,8 @@ userApiRequest schema req reqBody
|
|||||||
, iPreferRepresentation = representation
|
, iPreferRepresentation = representation
|
||||||
, iPreferSingleObjectParameter = singleObject
|
, iPreferSingleObjectParameter = singleObject
|
||||||
, iPreferCount = hasPrefer "count=exact"
|
, iPreferCount = hasPrefer "count=exact"
|
||||||
, iPreferResolution = if hasPrefer "resolution=merge-duplicates" then Just MergeDuplicates
|
, iPreferResolution = if hasPrefer (show MergeDuplicates) then Just MergeDuplicates
|
||||||
else if hasPrefer "resolution=ignore-duplicates" then Just IgnoreDuplicates
|
else if hasPrefer (show IgnoreDuplicates) then Just IgnoreDuplicates
|
||||||
else Nothing
|
else Nothing
|
||||||
, iFilters = filters
|
, iFilters = filters
|
||||||
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
||||||
|
|||||||
@@ -157,10 +157,10 @@ app dbStructure proc conf apiRequest =
|
|||||||
&& iPreferRepresentation apiRequest == Full
|
&& iPreferRepresentation apiRequest == Full
|
||||||
then return $ singularityError (toInteger nRows)
|
then return $ singularityError (toInteger nRows)
|
||||||
else do
|
else do
|
||||||
let stm = createWriteStatement sq mq
|
let pkCols = tablePKCols dbStructure tSchema tName
|
||||||
|
stm = createWriteStatement sq mq
|
||||||
(contentType == CTSingularJSON) isSingle
|
(contentType == CTSingularJSON) isSingle
|
||||||
(contentType == CTTextCSV) (iPreferRepresentation apiRequest)
|
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols
|
||||||
(tablePKCols dbStructure tSchema tName)
|
|
||||||
row <- H.query (toS pjRaw) stm
|
row <- H.query (toS pjRaw) stm
|
||||||
let (_, _, fs, body) = extractQueryResult row
|
let (_, _, fs, body) = extractQueryResult row
|
||||||
headers = catMaybes [
|
headers = catMaybes [
|
||||||
@@ -172,6 +172,9 @@ app dbStructure proc conf apiRequest =
|
|||||||
else Nothing
|
else Nothing
|
||||||
, Just . contentRangeH 1 0 $
|
, Just . contentRangeH 1 0 $
|
||||||
toInteger <$> if shouldCount then Just nRows else Nothing
|
toInteger <$> if shouldCount then Just nRows else Nothing
|
||||||
|
, if null pkCols
|
||||||
|
then Nothing
|
||||||
|
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
|
||||||
]
|
]
|
||||||
|
|
||||||
return . responseLBS status201 headers $
|
return . responseLBS status201 headers $
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys
|
|||||||
"'' AS total_result_set", -- when updateing it does not make sense
|
"'' AS total_result_set", -- when updateing it does not make sense
|
||||||
"pg_catalog.count(_postgrest_t) AS page_total",
|
"pg_catalog.count(_postgrest_t) AS page_total",
|
||||||
if wantHdrs
|
if wantHdrs
|
||||||
then locationF pKeys
|
then "coalesce(" <> locationF pKeys <> ", " <> noLocationF <> ")"
|
||||||
else noLocationF <> " AS header",
|
else noLocationF <> " AS header",
|
||||||
if rep == Full
|
if rep == Full
|
||||||
then bodyF <> " AS body"
|
then bodyF <> " AS body"
|
||||||
@@ -278,7 +278,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest
|
|||||||
requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType pKeys) onConflct logicForest returnings)) =
|
requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType pKeys) onConflct logicForest returnings)) =
|
||||||
unwords [
|
unwords [
|
||||||
("WITH " <> ignoredBody) `emptyOnFalse` not payloadIsEmpty,
|
("WITH " <> ignoredBody) `emptyOnFalse` not payloadIsEmpty,
|
||||||
"INSERT INTO ", fromQi qi, if payloadIsEmpty then " " else "(" <> cols <> ") ",
|
"INSERT INTO ", fromQi qi, if payloadIsEmpty then " " else "(" <> cols <> ")",
|
||||||
case (pType, payloadIsEmpty) of
|
case (pType, payloadIsEmpty) of
|
||||||
(PJArray _, True) -> "SELECT null WHERE false"
|
(PJArray _, True) -> "SELECT null WHERE false"
|
||||||
(PJObject, True) -> "DEFAULT VALUES"
|
(PJObject, True) -> "DEFAULT VALUES"
|
||||||
@@ -369,16 +369,12 @@ asBinaryF :: FieldName -> SqlFragment
|
|||||||
asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"
|
asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"
|
||||||
|
|
||||||
locationF :: [Text] -> SqlFragment
|
locationF :: [Text] -> SqlFragment
|
||||||
locationF pKeys =
|
locationF pKeys = [qc|(
|
||||||
"(" <>
|
WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1)
|
||||||
" WITH s AS (SELECT row_to_json(ss) as r from " <> sourceCTEName <> " as ss limit 1)" <>
|
SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))
|
||||||
" SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))" <>
|
FROM data CROSS JOIN json_each_text(data.row) AS json_data
|
||||||
" FROM s, json_each_text(s.r) AS json_data" <>
|
{("WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')") `emptyOnFalse` null pKeys}
|
||||||
(
|
)|]
|
||||||
if null pKeys
|
|
||||||
then ""
|
|
||||||
else " WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')"
|
|
||||||
) <> ")"
|
|
||||||
|
|
||||||
limitF :: NonnegRange -> SqlFragment
|
limitF :: NonnegRange -> SqlFragment
|
||||||
limitF r = if r == allRange
|
limitF r = if r == allRange
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ data ApiRequestError = ActionInappropriate
|
|||||||
| InvalidFilters
|
| InvalidFilters
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving (Eq, Show)
|
data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq
|
||||||
|
instance Show PreferResolution where
|
||||||
|
show MergeDuplicates = "resolution=merge-duplicates"
|
||||||
|
show IgnoreDuplicates = "resolution=ignore-duplicates"
|
||||||
|
|
||||||
data DbStructure = DbStructure {
|
data DbStructure = DbStructure {
|
||||||
dbTables :: [Table]
|
dbTables :: [Table]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ spec =
|
|||||||
{ "name": "C", "rank": 1 }
|
{ "name": "C", "rank": 1 }
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "INSERTs and UPDATEs row on composite pk conflict" $
|
it "INSERTs and UPDATEs row on composite pk conflict" $
|
||||||
@@ -41,7 +41,7 @@ spec =
|
|||||||
{ "first_name": "Peter S.", "last_name": "Yang", "salary": "$42,000.00", "company": null, "occupation": null }
|
{ "first_name": "Peter S.", "last_name": "Yang", "salary": "$42,000.00", "company": null, "occupation": null }
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "when Prefer: resolution=ignore-duplicates is specified" $ do
|
context "when Prefer: resolution=ignore-duplicates is specified" $ do
|
||||||
@@ -54,7 +54,7 @@ spec =
|
|||||||
{ "name": "PHP", "rank": 9 }
|
{ "name": "PHP", "rank": 9 }
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "INSERTs and ignores rows on composite pk conflict" $
|
it "INSERTs and ignores rows on composite pk conflict" $
|
||||||
@@ -66,25 +66,40 @@ spec =
|
|||||||
{ "first_name": "Sara M.", "last_name": "Torpey", "salary": "$60,000.00", "company": "Burstein-Applebee", "occupation": "Soil scientist" }
|
{ "first_name": "Sara M.", "last_name": "Torpey", "salary": "$60,000.00", "company": "Burstein-Applebee", "occupation": "Soil scientist" }
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "succeeds if the table has only PK cols and no other cols" $ do
|
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")]
|
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||||
[json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]
|
[json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[ { "id": 3} ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
|
[json|[ { "id": 3} ]|]
|
||||||
|
{ matchStatus = 201 ,
|
||||||
|
matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates",
|
||||||
|
matchContentTypeJson] }
|
||||||
|
|
||||||
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||||
[json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]
|
[json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
|
[json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]
|
||||||
|
{ matchStatus = 201 ,
|
||||||
|
matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates",
|
||||||
|
matchContentTypeJson] }
|
||||||
|
|
||||||
it "succeeds and ignores the Prefer: resolution header if the table has no PK" $
|
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")]
|
request methodPost "/no_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||||
[json|[ { "a": "1", "b": "0" } ]|]
|
[json|[ { "a": "1", "b": "0" } ]|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[ { "a": "1", "b": "0" } ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
|
[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 "with PUT" $ do
|
||||||
context "Restrictions" $ do
|
context "Restrictions" $ do
|
||||||
it "fails if Range is specified" $
|
it "fails if Range is specified" $
|
||||||
|
|||||||
Reference in New Issue
Block a user