Skip to content

Commit

Permalink
Add PostGIS GeoJSON support
Browse files Browse the repository at this point in the history
Works for GET, POST, PATCH, PUT, DELETE, RPC.
  • Loading branch information
steve-chavez committed Jun 23, 2022
1 parent 86c40e8 commit 870f7a3
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2268, Allow returning XML from single-column queries - @fjf2002
- #2300, RPC POST for function w/single unnamed XML param #2300 - @fjf2002
- #1959, Bulk update with PATCH - @steve-chavez
- #1564, Allow geojson output by specifying the `Accept: application/geo+json` media type - @steve-chavez
+ Requires postgis >= 3.0
+ Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`.
+ Resource embedding works and the embedded rows will go into the `properties` key
+ In case of multiple geometries in the same table, you can choose which one will go into the `geometry` key with the usual `?select` query parameter.

### Fixed

Expand Down
12 changes: 6 additions & 6 deletions default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ let

postgresqlVersions =
[
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14; }
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13; }
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12; }
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11; }
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10; }
{ name = "postgresql-9.6"; postgresql = pkgs.postgresql_9_6; }
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10.withPackages (p: [ p.postgis ]); }
{ name = "postgresql-9.6"; postgresql = pkgs.postgresql_9_6.withPackages (p: [ p.postgis ]); }
];

patches =
Expand Down
1 change: 1 addition & 0 deletions postgrest.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ test-suite spec
Feature.Query.JsonOperatorSpec
Feature.Query.MultipleSchemaSpec
Feature.Query.ErrorSpec
Feature.Query.PostGISSpec
Feature.Query.QueryLimitedSpec
Feature.Query.QuerySpec
Feature.Query.RangeSpec
Expand Down
3 changes: 3 additions & 0 deletions src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ handleRead headersOnly identifier context@RequestContext{..} = do
(shouldCount iPreferCount)
(iAcceptContentType == CTTextCSV)
(iAcceptContentType == CTTextXML)
(iAcceptContentType == CTGeoJSON)
bField
configDbPreparedStatements

Expand Down Expand Up @@ -464,6 +465,7 @@ handleInvoke invMethod proc context@RequestContext{..} = do
(iAcceptContentType == CTSingularJSON)
(iAcceptContentType == CTTextCSV)
(iAcceptContentType == CTTextXML)
(iAcceptContentType == CTGeoJSON)
(iPreferParameters == Just MultipleObjects)
bField
(configDbPreparedStatements ctxConfig)
Expand Down Expand Up @@ -551,6 +553,7 @@ writeQuery mutation identifier@QualifiedIdentifier{..} isInsert pkCols context@R
(iAcceptContentType ctxApiRequest == CTSingularJSON)
isInsert
(iAcceptContentType ctxApiRequest == CTTextCSV)
(iAcceptContentType ctxApiRequest == CTGeoJSON)
(iPreferRepresentation ctxApiRequest)
pkCols
(configDbPreparedStatements ctxConfig)
Expand Down
3 changes: 3 additions & 0 deletions src/PostgREST/ContentType.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Protolude
data ContentType
= CTApplicationJSON
| CTSingularJSON
| CTGeoJSON
| CTTextCSV
| CTTextPlain
| CTTextXML
Expand All @@ -40,6 +41,7 @@ toHeader ct = (hContentType, toMime ct <> charset)
-- | Convert from ContentType to a ByteString representing the mime type
toMime :: ContentType -> ByteString
toMime CTApplicationJSON = "application/json"
toMime CTGeoJSON = "application/geo+json"
toMime CTTextCSV = "text/csv"
toMime CTTextPlain = "text/plain"
toMime CTTextXML = "text/xml"
Expand All @@ -55,6 +57,7 @@ decodeContentType :: BS.ByteString -> ContentType
decodeContentType ct =
case BS.takeWhile (/= BS.c2w ';') ct of
"application/json" -> CTApplicationJSON
"application/geo+json" -> CTGeoJSON
"text/csv" -> CTTextCSV
"text/plain" -> CTTextPlain
"text/xml" -> CTTextXML
Expand Down
4 changes: 4 additions & 0 deletions src/PostgREST/Query/SqlFragment.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module PostgREST.Query.SqlFragment
, SqlFragment
, asBinaryF
, asCsvF
, asGeoJsonF
, asJsonF
, asJsonSingleF
, asXmlF
Expand Down Expand Up @@ -182,6 +183,9 @@ asJsonSingleF returnsScalar
asXmlF :: FieldName -> SqlFragment
asXmlF fieldName = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')"

asGeoJsonF :: SqlFragment
asGeoJsonF = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))"

asBinaryF :: FieldName -> SqlFragment
asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"

Expand Down
15 changes: 9 additions & 6 deletions src/PostgREST/Query/Statements.hs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ import Protolude
-}
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either Error [GucHeader], Either Error (Maybe Status))

createWriteStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool ->
createWriteStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Bool ->
PreferRepresentation -> [Text] -> Bool ->
SQL.Statement () ResultsWithCount
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys =
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv asGeoJson rep pKeys =
SQL.dynamicallyParameterized snippet decodeStandard
where
snippet =
Expand Down Expand Up @@ -74,6 +74,7 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
bodyF
| rep /= Full = "''"
| asCsv = asCsvF
| asGeoJson = asGeoJsonF
| wantSingle = asJsonSingleF False
| otherwise = asJsonF False

Expand All @@ -86,9 +87,9 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
decodeStandard =
fromMaybe (Nothing, 0, [], mempty, Right [], Right Nothing) <$> HD.rowMaybe standardRow

createReadStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
createReadStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
SQL.Statement () ResultsWithCount
createReadStatement selectQuery countQuery isSingle countTotal asCsv asXml binaryField =
createReadStatement selectQuery countQuery isSingle countTotal asCsv asXml asGeoJson binaryField =
SQL.dynamicallyParameterized snippet decodeStandard
where
snippet =
Expand All @@ -109,6 +110,7 @@ createReadStatement selectQuery countQuery isSingle countTotal asCsv asXml binar
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF False
| asGeoJson = asGeoJsonF
| isJust binaryField && asXml = asXmlF $ fromJust binaryField
| isJust binaryField = asBinaryF $ fromJust binaryField
| otherwise = asJsonF False
Expand All @@ -130,9 +132,9 @@ standardRow = (,,,,,) <$> nullableColumn HD.int8 <*> column HD.int8
type ProcResults = (Maybe Int64, Int64, ByteString, Either Error [GucHeader], Either Error (Maybe Status))

callProcStatement :: Bool -> Bool -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
SQL.Statement () ProcResults
callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQuery countTotal asSingle asCsv asXml multObjects binaryField =
callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQuery countTotal asSingle asCsv asXml asGeoJson multObjects binaryField =
SQL.dynamicallyParameterized snippet decodeProc
where
snippet =
Expand All @@ -152,6 +154,7 @@ callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQue
bodyF
| asSingle = asJsonSingleF returnsScalar
| asCsv = asCsvF
| asGeoJson = asGeoJsonF
| isJust binaryField && asXml = asXmlF $ fromJust binaryField
| isJust binaryField = asBinaryF $ fromJust binaryField
| returnsSingle
Expand Down
2 changes: 1 addition & 1 deletion src/PostgREST/Request/ApiRequest.hs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ requestContentTypes conf action path =
++ rawContentTypes conf
++ [CTOpenAPI | pIsRootSpec path]
defaultContentTypes =
[CTApplicationJSON, CTSingularJSON, CTTextCSV]
[CTApplicationJSON, CTSingularJSON, CTGeoJSON, CTTextCSV]

rawContentTypes :: AppConfig -> [ContentType]
rawContentTypes AppConfig{..} =
Expand Down
Loading

0 comments on commit 870f7a3

Please sign in to comment.