undefined-keys=apply-defaults to missing=default
This commit is contained in:
committed by
Steve Chavez
parent
46fd856fc6
commit
7629eff51d
+1
-1
@@ -24,7 +24,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
+ When the client sends the request header specified in the config it will be included in the response headers.
|
+ When the client sends the request header specified in the config it will be included in the response headers.
|
||||||
- #2694, Make `db-root-spec` stable. - @steve-chavez
|
- #2694, Make `db-root-spec` stable. - @steve-chavez
|
||||||
+ This can be used to override the OpenAPI spec with a custom database function
|
+ This can be used to override the OpenAPI spec with a custom database function
|
||||||
- #1567, On bulk inserts with `?columns`, undefined json keys can get columns' DEFAULT values by using the `Prefer: undefined-keys=apply-defaults` header - @steve-chavez
|
- #1567, On bulk inserts, missing values can get the column DEFAULT by using the `Prefer: missing=default` header - @steve-chavez
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
module PostgREST.ApiRequest.Preferences
|
module PostgREST.ApiRequest.Preferences
|
||||||
( Preferences(..)
|
( Preferences(..)
|
||||||
, PreferCount(..)
|
, PreferCount(..)
|
||||||
, PreferUndefinedKeys(..)
|
, PreferMissing(..)
|
||||||
, PreferParameters(..)
|
, PreferParameters(..)
|
||||||
, PreferRepresentation(..)
|
, PreferRepresentation(..)
|
||||||
, PreferResolution(..)
|
, PreferResolution(..)
|
||||||
@@ -34,7 +34,7 @@ import Protolude
|
|||||||
-- >>> deriving instance Show PreferParameters
|
-- >>> deriving instance Show PreferParameters
|
||||||
-- >>> deriving instance Show PreferCount
|
-- >>> deriving instance Show PreferCount
|
||||||
-- >>> deriving instance Show PreferTransaction
|
-- >>> deriving instance Show PreferTransaction
|
||||||
-- >>> deriving instance Show PreferUndefinedKeys
|
-- >>> deriving instance Show PreferMissing
|
||||||
-- >>> deriving instance Show Preferences
|
-- >>> deriving instance Show Preferences
|
||||||
|
|
||||||
-- | Preferences recognized by the application.
|
-- | Preferences recognized by the application.
|
||||||
@@ -45,7 +45,7 @@ data Preferences
|
|||||||
, preferParameters :: Maybe PreferParameters
|
, preferParameters :: Maybe PreferParameters
|
||||||
, preferCount :: Maybe PreferCount
|
, preferCount :: Maybe PreferCount
|
||||||
, preferTransaction :: Maybe PreferTransaction
|
, preferTransaction :: Maybe PreferTransaction
|
||||||
, preferUndefinedKeys :: Maybe PreferUndefinedKeys
|
, preferMissing :: Maybe PreferMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
@@ -60,19 +60,19 @@ data Preferences
|
|||||||
-- , preferParameters = Nothing
|
-- , preferParameters = Nothing
|
||||||
-- , preferCount = Just ExactCount
|
-- , preferCount = Just ExactCount
|
||||||
-- , preferTransaction = Nothing
|
-- , preferTransaction = Nothing
|
||||||
-- , preferUndefinedKeys = Nothing
|
-- , preferMissing = Nothing
|
||||||
-- }
|
-- }
|
||||||
--
|
--
|
||||||
-- Multiple headers can also be used:
|
-- Multiple headers can also be used:
|
||||||
--
|
--
|
||||||
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact")]
|
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null")]
|
||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Just IgnoreDuplicates
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
-- , preferRepresentation = None
|
-- , preferRepresentation = None
|
||||||
-- , preferParameters = Nothing
|
-- , preferParameters = Nothing
|
||||||
-- , preferCount = Just ExactCount
|
-- , preferCount = Just ExactCount
|
||||||
-- , preferTransaction = Nothing
|
-- , preferTransaction = Nothing
|
||||||
-- , preferUndefinedKeys = Nothing
|
-- , preferMissing = Just ApplyNulls
|
||||||
-- }
|
-- }
|
||||||
--
|
--
|
||||||
-- If a preference is set more than once, only the first is used:
|
-- If a preference is set more than once, only the first is used:
|
||||||
@@ -97,14 +97,14 @@ data Preferences
|
|||||||
--
|
--
|
||||||
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
||||||
--
|
--
|
||||||
-- >>> pPrint $ fromHeaders [("prefer", "count=exact, tx=commit ,return=representation , undefined-keys=apply-defaults")]
|
-- >>> pPrint $ fromHeaders [("prefer", "count=exact, tx=commit ,return=representation , missing=default")]
|
||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Nothing
|
-- { preferResolution = Nothing
|
||||||
-- , preferRepresentation = Full
|
-- , preferRepresentation = Full
|
||||||
-- , preferParameters = Nothing
|
-- , preferParameters = Nothing
|
||||||
-- , preferCount = Just ExactCount
|
-- , preferCount = Just ExactCount
|
||||||
-- , preferTransaction = Just Commit
|
-- , preferTransaction = Just Commit
|
||||||
-- , preferUndefinedKeys = Just ApplyDefaults
|
-- , preferMissing = Just ApplyDefaults
|
||||||
-- }
|
-- }
|
||||||
--
|
--
|
||||||
fromHeaders :: [HTTP.Header] -> Preferences
|
fromHeaders :: [HTTP.Header] -> Preferences
|
||||||
@@ -115,7 +115,7 @@ fromHeaders headers =
|
|||||||
, preferParameters = parsePrefs [SingleObject, MultipleObjects]
|
, preferParameters = parsePrefs [SingleObject, MultipleObjects]
|
||||||
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
||||||
, preferTransaction = parsePrefs [Commit, Rollback]
|
, preferTransaction = parsePrefs [Commit, Rollback]
|
||||||
, preferUndefinedKeys = parsePrefs [ApplyDefaults, IgnoreDefaults]
|
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
|
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
|
||||||
@@ -215,13 +215,13 @@ instance ToAppliedHeader PreferTransaction
|
|||||||
-- |
|
-- |
|
||||||
-- How to handle the insertion/update when the keys specified in ?columns are not present
|
-- How to handle the insertion/update when the keys specified in ?columns are not present
|
||||||
-- in the json body.
|
-- in the json body.
|
||||||
data PreferUndefinedKeys
|
data PreferMissing
|
||||||
= ApplyDefaults -- ^ Use the default column value for the unspecified keys.
|
= ApplyDefaults -- ^ Use the default column value for missing values.
|
||||||
| IgnoreDefaults -- ^ Inserts: null values / Updates: the keys are not SET to any value
|
| ApplyNulls -- ^ Use the null value for missing values.
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
instance ToHeaderValue PreferUndefinedKeys where
|
instance ToHeaderValue PreferMissing where
|
||||||
toHeaderValue ApplyDefaults = "undefined-keys=apply-defaults"
|
toHeaderValue ApplyDefaults = "missing=default"
|
||||||
toHeaderValue IgnoreDefaults = "undefined-keys=ignore-defaults"
|
toHeaderValue ApplyNulls = "missing=null"
|
||||||
|
|
||||||
instance ToAppliedHeader PreferUndefinedKeys
|
instance ToAppliedHeader PreferMissing
|
||||||
|
|||||||
@@ -533,7 +533,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=preferences, ..} sCache readReq =
|
|||||||
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
||||||
tbl = HM.lookup qi $ dbTables sCache
|
tbl = HM.lookup qi $ dbTables sCache
|
||||||
typedColumnsOrError = resolveOrError tbl `traverse` S.toList iColumns
|
typedColumnsOrError = resolveOrError tbl `traverse` S.toList iColumns
|
||||||
applyDefaults = preferences.preferUndefinedKeys == Just ApplyDefaults
|
applyDefaults = preferences.preferMissing == Just ApplyDefaults
|
||||||
|
|
||||||
resolveOrError :: Maybe Table -> FieldName -> Either ApiRequestError TypedField
|
resolveOrError :: Maybe Table -> FieldName -> Either ApiRequestError TypedField
|
||||||
resolveOrError Nothing _ = Left NotFound
|
resolveOrError Nothing _ = Left NotFound
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan} ctxApiReques
|
|||||||
Nothing
|
Nothing
|
||||||
else
|
else
|
||||||
toAppliedHeader <$> preferResolution
|
toAppliedHeader <$> preferResolution
|
||||||
, toAppliedHeader <$> preferUndefinedKeys
|
, toAppliedHeader <$> preferMissing
|
||||||
]
|
]
|
||||||
|
|
||||||
if preferRepresentation == Full then
|
if preferRepresentation == Full then
|
||||||
@@ -128,7 +128,7 @@ updateResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet
|
|||||||
contentRangeHeader =
|
contentRangeHeader =
|
||||||
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||||
headers = catMaybes [contentRangeHeader, toAppliedHeader <$> preferUndefinedKeys]
|
headers = catMaybes [contentRangeHeader, toAppliedHeader <$> preferMissing]
|
||||||
|
|
||||||
if preferRepresentation == Full then
|
if preferRepresentation == Full then
|
||||||
response HTTP.status200
|
response HTTP.status200
|
||||||
|
|||||||
@@ -451,11 +451,11 @@ spec actualPgVersion = do
|
|||||||
"asdf",
|
"asdf",
|
||||||
{"id": 205, "body": "zzz"}]|] `shouldRespondWith` 400
|
{"id": 205, "body": "zzz"}]|] `shouldRespondWith` 400
|
||||||
|
|
||||||
context "apply defaults on undefined keys" $ do
|
context "apply defaults on missing values" $ do
|
||||||
-- inserting the array fails on pg 9.6, but the feature should work normally
|
-- inserting the array fails on pg 9.6, but the feature should work normally
|
||||||
when (actualPgVersion >= pgVersion100) $
|
when (actualPgVersion >= pgVersion100) $
|
||||||
it "inserts table default values(field-with_sep) when json keys are undefined" $
|
it "inserts table default values(field-with_sep) when json keys are undefined" $
|
||||||
request methodPost "/complex_items?columns=id,name,field-with_sep,arr_data" [("Prefer", "return=representation"), ("Prefer", "undefined-keys=apply-defaults")]
|
request methodPost "/complex_items?columns=id,name,field-with_sep,arr_data" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
||||||
[json|[
|
[json|[
|
||||||
{"id": 4, "name": "Vier"},
|
{"id": 4, "name": "Vier"},
|
||||||
{"id": 5, "name": "Funf", "arr_data": null},
|
{"id": 5, "name": "Funf", "arr_data": null},
|
||||||
@@ -468,11 +468,11 @@ spec actualPgVersion = do
|
|||||||
{"id": 6, "name": "Sechs", "field-with_sep": 6, "settings":null,"arr_data":[1,2,3]}
|
{"id": 6, "name": "Sechs", "field-with_sep": 6, "settings":null,"arr_data":[1,2,3]}
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
, matchHeaders = ["Preference-Applied" <:> "undefined-keys=apply-defaults"]
|
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "inserts view default values(field-with_sep) when json keys are undefined" $
|
it "inserts view default values(field-with_sep) when json keys are undefined" $
|
||||||
request methodPost "/complex_items_view?columns=id,name" [("Prefer", "return=representation"), ("Prefer", "undefined-keys=apply-defaults")]
|
request methodPost "/complex_items_view?columns=id,name" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
||||||
[json|[
|
[json|[
|
||||||
{"id": 7, "name": "Sieben"},
|
{"id": 7, "name": "Sieben"},
|
||||||
{"id": 8}
|
{"id": 8}
|
||||||
@@ -483,16 +483,16 @@ spec actualPgVersion = do
|
|||||||
{"id": 8, "name": "Default", "field-with_sep": 1, "settings":null,"arr_data":null}
|
{"id": 8, "name": "Default", "field-with_sep": 1, "settings":null,"arr_data":null}
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
, matchHeaders = ["Preference-Applied" <:> "undefined-keys=apply-defaults"]
|
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "doesn't insert json duplicate keys(since it uses jsonb)" $
|
it "doesn't insert json duplicate keys(since it uses jsonb)" $
|
||||||
request methodPost "/tbl_w_json?columns=id,data" [("Prefer", "return=representation"), ("Prefer", "undefined-keys=apply-defaults")]
|
request methodPost "/tbl_w_json?columns=id,data" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
||||||
[json| { "data": { "a": 1, "a": 2 }, "id": 3 } |]
|
[json| { "data": { "a": 1, "a": 2 }, "id": 3 } |]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| [ { "data": { "a": 2 }, "id": 3 } ] |]
|
[json| [ { "data": { "a": 2 }, "id": 3 } ] |]
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
, matchHeaders = ["Preference-Applied" <:> "undefined-keys=apply-defaults"]
|
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "inserts json that has duplicate keys" $ do
|
it "inserts json that has duplicate keys" $ do
|
||||||
|
|||||||
@@ -330,46 +330,46 @@ spec = do
|
|||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
context "apply defaults on undefined keys" $ do
|
context "apply defaults on missing values" $ do
|
||||||
it "updates table using default values(field-with_sep) when json keys are undefined" $ do
|
it "updates table using default values(field-with_sep) when json keys are undefined" $ do
|
||||||
request methodPatch "/complex_items?id=eq.3&columns=name,field-with_sep"
|
request methodPatch "/complex_items?id=eq.3&columns=name,field-with_sep"
|
||||||
[("Prefer", "return=representation"), ("Prefer", "undefined-keys=apply-defaults")]
|
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
||||||
[json|{"name": "Tres"}|]
|
[json|{"name": "Tres"}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[
|
[json|[
|
||||||
{"id":3,"name":"Tres","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}
|
{"id":3,"name":"Tres","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Preference-Applied" <:> "undefined-keys=apply-defaults"]
|
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "updates with limit/offset using table default values(field-with_sep) when json keys are undefined" $ do
|
it "updates with limit/offset using table default values(field-with_sep) when json keys are undefined" $ do
|
||||||
request methodPatch "/complex_items?select=id,name&columns=name,field-with_sep&limit=1&offset=2&order=id"
|
request methodPatch "/complex_items?select=id,name&columns=name,field-with_sep&limit=1&offset=2&order=id"
|
||||||
[("Prefer", "return=representation"), ("Prefer", "undefined-keys=apply-defaults")]
|
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
||||||
[json|{"name": "Tres"}|]
|
[json|{"name": "Tres"}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[
|
[json|[
|
||||||
{"id":3,"name":"Tres"}
|
{"id":3,"name":"Tres"}
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Preference-Applied" <:> "undefined-keys=apply-defaults"]
|
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "updates table default values(field-with_sep) when json keys are undefined" $ do
|
it "updates table default values(field-with_sep) when json keys are undefined" $ do
|
||||||
request methodPatch "/complex_items?id=eq.3&columns=name,field-with_sep"
|
request methodPatch "/complex_items?id=eq.3&columns=name,field-with_sep"
|
||||||
[("Prefer", "return=representation"), ("Prefer", "undefined-keys=apply-defaults")]
|
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
||||||
[json|{"name": "Tres"}|]
|
[json|{"name": "Tres"}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[
|
[json|[
|
||||||
{"id":3,"name":"Tres","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}
|
{"id":3,"name":"Tres","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Preference-Applied" <:> "undefined-keys=apply-defaults"]
|
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "updates view default values(field-with_sep) when json keys are undefined" $
|
it "updates view default values(field-with_sep) when json keys are undefined" $
|
||||||
request methodPatch "/complex_items_view?id=eq.3&columns=arr_data,name"
|
request methodPatch "/complex_items_view?id=eq.3&columns=arr_data,name"
|
||||||
[("Prefer", "return=representation"), ("Prefer", "undefined-keys=apply-defaults")]
|
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
||||||
[json|
|
[json|
|
||||||
{"arr_data":null}
|
{"arr_data":null}
|
||||||
|]
|
|]
|
||||||
@@ -378,7 +378,7 @@ spec = do
|
|||||||
{"id":3,"name":"Default","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":null,"field-with_sep":3}
|
{"id":3,"name":"Default","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":null,"field-with_sep":3}
|
||||||
]|]
|
]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Preference-Applied" <:> "undefined-keys=apply-defaults"]
|
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "tables with self reference foreign keys" $ do
|
context "tables with self reference foreign keys" $ do
|
||||||
|
|||||||
Reference in New Issue
Block a user