1414
1515PostGIS support for PostGraphile v5.
1616
17- Automatically generates GraphQL types for PostGIS geometry and geography columns, including GeoJSON scalar types, dimension-aware interfaces, and subtype-specific fields (coordinates, points, rings, etc.).
17+ Automatically generates GraphQL types for PostGIS ` geometry ` and
18+ ` geography ` columns — GeoJSON scalars, dimension-aware interfaces,
19+ subtype-specific fields (coordinates, points, rings, etc.), plus
20+ measurement / transformation / aggregate fields — so spatial data
21+ flows through your API with the same ergonomics as every other column.
1822
1923## The problem
2024
21- Working with PostGIS from an app is usually painful for one specific
22- reason: ** you end up juggling large amounts of GeoJSON across tables on
23- the client ** . You fetch every clinic as GeoJSON, fetch every county
24- polygon as GeoJSON, and then — in the browser — loop through them
25- yourself to figure out which clinic sits inside which county. Every
26- query, every count, every page of results becomes a client-side
27- geometry problem .
28-
29- An ORM generated automatically from your database schema can't fix this
30- on its own. It sees a ` geometry ` column and stops there — it has no
31- idea that "clinics inside a county" is the question you actually want
32- to ask. Foreign keys tell it how tables relate by equality; nothing
33- tells it how tables relate * spatially * .
34-
35- So we added the missing primitive: a ** spatial relation ** . You declare,
36- on the database column, that ` clinics.location ` is "inside"
37- ` counties.geom ` , and the generated GraphQL schema + ORM gain a
38- first-class ` where: { county: { some: { … } } } ` shape that runs the
39- join server-side, in one SQL query, using PostGIS and a GIST index. No
40- GeoJSON on the wire, no client-side geometry, and the relation composes
41- with the rest of your ` where: ` the same way a foreign-key relation
42- would .
25+ PostGIS is a first-class spatial database, but the default PostGraphile
26+ schema doesn't know what to do with it: a ` geometry ` column looks
27+ opaque to the generated GraphQL types, there's no scalar for GeoJSON,
28+ no concrete type for ` Point ` vs ` Polygon ` , no way to read back
29+ coordinates, lengths, areas, or bounding boxes without writing your
30+ own computed columns. You end up hand-rolling GraphQL types,
31+ client-side parsers, and one-off SQL helpers for every spatial column .
32+
33+ ` graphile-postgis ` closes that gap. The plugin detects the PostGIS
34+ extension (even when installed in a non- ` public ` schema), registers a
35+ ` GeoJSON ` scalar, and generates a full type hierarchy — ` Geometry ` /
36+ ` Geography ` interfaces, dimension-aware interfaces ( ` XY ` , ` XYZ ` , ` XYM ` ,
37+ ` XYZM ` ), and concrete subtypes ( ` Point ` , ` LineString ` , ` Polygon ` ,
38+ ` MultiPoint ` , ` MultiLineString ` , ` MultiPolygon ` , ` GeometryCollection ` )
39+ — with subtype-specific fields, measurement fields, transformation
40+ fields, and aggregate fields already wired up. If PostGIS isn't
41+ installed, the plugin degrades gracefully instead of failing the
42+ schema build.
43+
44+ Cross-table spatial joins — the "which clinics are inside this county"
45+ shape — are a separate feature built on top of the type layer; see
46+ [ Spatial relations ] ( #spatial-relations ) .
4347
4448## Installation
4549
@@ -60,24 +64,79 @@ const preset = {
6064## Features
6165
6266- GeoJSON scalar type for input/output
63- - GraphQL interfaces for geometry and geography base types
64- - Dimension-aware interfaces (XY, XYZ, XYM, XYZM)
65- - Concrete types for all geometry subtypes: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection
66- - Subtype-specific fields (x/y/z for Points, points for LineStrings, exterior/interiors for Polygons, etc.)
67- - Geography-aware field naming (longitude/latitude/height instead of x/y/z)
68- - Cross-table spatial relations via ` @spatialRelation ` smart tags (see below)
69- - Graceful degradation when PostGIS is not installed
70-
71- ## Spatial relations via smart tags
72-
73- You declare a ** spatial relation** with a ` @spatialRelation ` smart tag
74- on a ` geometry ` or ` geography ` column. The plugin turns that tag into a
67+ - GraphQL interfaces for ` geometry ` and ` geography ` base types
68+ - Dimension-aware interfaces (` XY ` , ` XYZ ` , ` XYM ` , ` XYZM ` )
69+ - Concrete types for all geometry subtypes: ` Point ` , ` LineString ` , ` Polygon ` , ` MultiPoint ` , ` MultiLineString ` , ` MultiPolygon ` , ` GeometryCollection `
70+ - Subtype-specific fields (` x ` / ` y ` / ` z ` for points, ` points ` for line strings, ` exterior ` / ` interiors ` for polygons, etc.)
71+ - Geography-aware field naming (` longitude ` / ` latitude ` / ` height ` instead of ` x ` / ` y ` / ` z ` )
72+ - Measurement fields (` length ` , ` area ` , ` perimeter ` ) computed geodesically from GeoJSON
73+ - Transformation fields (` centroid ` , ` bbox ` , ` numPoints ` )
74+ - PostGIS aggregate fields (` stExtent ` , ` stUnion ` , ` stCollect ` , ` stConvexHull ` )
75+ - Cross-table [ spatial relations] ( #spatial-relations ) via ` @spatialRelation ` smart tags
76+ - Auto-detects PostGIS in non-` public ` schemas and degrades gracefully when the extension is missing
77+
78+ ## Core PostGIS type support
79+
80+ Out of the box, the preset turns every ` geometry ` / ` geography ` column
81+ into a real, typed GraphQL field:
82+
83+ - ** GeoJSON scalar + subtype objects.** Values are serialised as
84+ GeoJSON. Each concrete subtype (` Point ` , ` Polygon ` , …) is its own
85+ object type and exposes its natural accessor fields — e.g. ` Point `
86+ has ` x ` / ` y ` / ` z ` (or ` longitude ` / ` latitude ` / ` height ` on a
87+ ` geography ` column), ` Polygon ` has ` exterior ` and ` interiors ` ,
88+ ` LineString ` has ` points ` .
89+ - ** Dimension-aware interfaces.** ` XY ` , ` XYZ ` , ` XYM ` , and ` XYZM `
90+ interfaces let clients request coordinates without caring about the
91+ specific subtype.
92+ - ** Measurement, transformation, and aggregate fields.** Geometry
93+ types expose ` length ` , ` area ` , ` perimeter ` , ` centroid ` , ` bbox ` , and
94+ ` numPoints ` ; aggregate types expose ` stExtent ` , ` stUnion ` ,
95+ ` stCollect ` , and ` stConvexHull ` .
96+ - ** Graceful degradation.** If the ` postgis ` extension isn't installed
97+ in the target database, the plugin skips type registration instead
98+ of breaking the schema build.
99+
100+ For cross-table spatial joins — e.g. "clinics inside a county" — see
101+ [ Spatial relations] ( #spatial-relations ) .
102+
103+ ## Spatial relations
104+
105+ ### The problem
106+
107+ Even with GeoJSON types on every column, working with PostGIS from an
108+ app is usually painful for one specific reason: ** you end up juggling
109+ large amounts of GeoJSON across tables on the client** . You fetch
110+ every clinic as GeoJSON, fetch every county polygon as GeoJSON, and
111+ then — in the browser — loop through them yourself to figure out
112+ which clinic sits inside which county. Every query, every count,
113+ every page of results becomes a client-side geometry problem.
114+
115+ An ORM generated automatically from your database schema can't fix
116+ this on its own. It sees a ` geometry ` column and stops there — it has
117+ no idea that "clinics inside a county" is the question you actually
118+ want to ask. Foreign keys tell it how tables relate by equality;
119+ nothing tells it how tables relate * spatially* .
120+
121+ So we added the missing primitive: a ** spatial relation** . You
122+ declare, on the database column, that ` clinics.location ` is "inside"
123+ ` counties.geom ` , and the generated GraphQL schema + ORM gain a
124+ first-class ` where: { county: { some: { … } } } ` shape that runs the
125+ join server-side, in one SQL query, using PostGIS and a GIST index.
126+ No GeoJSON on the wire, no client-side geometry, and the relation
127+ composes with the rest of your ` where: ` the same way a foreign-key
128+ relation would.
129+
130+ ### Smart-tag overview
131+
132+ You declare a spatial relation with a ` @spatialRelation ` smart tag on
133+ a ` geometry ` or ` geography ` column. The plugin turns that tag into a
75134virtual relation on the owning table: a new field on the table's
76- generated ` where ` input that runs a PostGIS join server-side. You write
77- one line of SQL once; the generated ORM and GraphQL schema pick it up
78- automatically.
135+ generated ` where ` input that runs a PostGIS join server-side. You
136+ write one line of SQL once; the generated ORM and GraphQL schema pick
137+ it up automatically.
79138
80- ### At a glance
139+ #### At a glance
81140
82141** Before** — GeoJSON juggling on the client:
83142
@@ -114,7 +173,7 @@ two columns.
114173
115174### Declaring a relation
116175
117- #### Tag grammar
176+ ** Tag grammar**
118177
119178```
120179@spatialRelation <relationName> <targetRef> <operator> [<paramName>]
@@ -137,7 +196,7 @@ Both sides of the relation must be `geometry` or `geography`, and they
137196must share the ** same** base codec — you cannot mix ` geometry ` and
138197` geography ` .
139198
140- #### Multiple relations on one column
199+ ** Multiple relations on one column**
141200
142201Stack tags. Each line becomes its own field on the owning table's
143202` where ` input:
@@ -179,7 +238,7 @@ share a `<relationName>`.
179238
180239### Using the generated ` where ` shape
181240
182- #### Through the ORM
241+ ** Through the ORM**
183242
184243``` ts
185244// "Clinics inside any county named 'Bay County'"
@@ -191,7 +250,7 @@ await orm.telemedicineClinic
191250 .execute ();
192251```
193252
194- #### Through GraphQL
253+ ** Through GraphQL**
195254
196255The connection argument is ` where: ` at the GraphQL layer too — same
197256name, same tree. Only the generated input ** type** keeps the word
@@ -207,7 +266,7 @@ name, same tree. Only the generated input **type** keeps the word
207266}
208267```
209268
210- #### ` some ` / ` every ` / ` none `
269+ ** ` some ` / ` every ` / ` none ` **
211270
212271Every 2-argument relation exposes three modes. They mean what you'd
213272expect, backed by ` EXISTS ` / ` NOT EXISTS ` :
@@ -225,7 +284,7 @@ row exists, any row will do" — so for `@spatialRelation county …
225284st_within`, clinics whose point is inside zero counties are correctly
226285excluded.
227286
228- #### Parametric operators (` st_dwithin ` + ` distance ` )
287+ ** Parametric operators (` st_dwithin ` + ` distance ` )**
229288
230289Parametric relations add a ** required** ` distance: Float! ` field next
231290to ` some ` / ` every ` / ` none ` . The distance parametrises the join
0 commit comments