graphile-postgis 2.10.1 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,9 +12,27 @@
12
12
  <a href="https://www.npmjs.com/package/graphile-postgis"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphile%2Fgraphile-postgis%2Fpackage.json"/></a>
13
13
  </p>
14
14
 
15
- PostGIS support for PostGraphile v5.
16
-
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.).
15
+ A full PostGIS integration for PostGraphile v5. Turns every
16
+ `geometry` / `geography` column into a typed, introspectable GraphQL
17
+ field with GeoJSON scalars, subtype-specific fields, measurement
18
+ helpers, spatial filters, aggregates, and cross-table **spatial
19
+ relations** — and wires the whole thing into the generated ORM so you
20
+ can query spatial data the same way you query anything else.
21
+
22
+ ## Table of contents
23
+
24
+ - [Installation](#installation)
25
+ - [Usage](#usage)
26
+ - [Features at a glance](#features-at-a-glance)
27
+ - [GeoJSON scalar and typed geometry columns](#geojson-scalar-and-typed-geometry-columns)
28
+ - [Dimension-aware interfaces and subtype fields](#dimension-aware-interfaces-and-subtype-fields)
29
+ - [Measurement fields (`length`, `area`, `perimeter`)](#measurement-fields-length-area-perimeter)
30
+ - [Transformation fields (`centroid`, `bbox`, `numPoints`)](#transformation-fields-centroid-bbox-numpoints)
31
+ - [Per-column spatial filters](#per-column-spatial-filters)
32
+ - [PostGIS aggregate fields](#postgis-aggregate-fields)
33
+ - [Spatial relations (`@spatialRelation`)](#spatial-relations-spatialrelation)
34
+ - [Graceful degradation](#graceful-degradation)
35
+ - [License](#license)
18
36
 
19
37
  ## Installation
20
38
 
@@ -24,23 +42,489 @@ npm install graphile-postgis
24
42
 
25
43
  ## Usage
26
44
 
27
- ```typescript
45
+ ```ts
28
46
  import { GraphilePostgisPreset } from 'graphile-postgis';
29
47
 
30
48
  const preset = {
31
- extends: [GraphilePostgisPreset]
49
+ extends: [GraphilePostgisPreset],
32
50
  };
33
51
  ```
34
52
 
35
- ## Features
53
+ The preset bundles every plugin listed below. You can also import each
54
+ plugin individually (`PostgisCodecPlugin`, `PostgisRegisterTypesPlugin`,
55
+ `PostgisGeometryFieldsPlugin`, `PostgisMeasurementFieldsPlugin`,
56
+ `PostgisTransformationFieldsPlugin`, `PostgisAggregatePlugin`,
57
+ `PostgisSpatialRelationsPlugin`, …) if you prefer à-la-carte.
58
+
59
+ ## Features at a glance
60
+
61
+ - **GeoJSON scalar** for input and output on every `geometry` /
62
+ `geography` column.
63
+ - **Full type hierarchy** — `Geometry` / `Geography` interfaces,
64
+ dimension-aware interfaces (`XY`, `XYZ`, `XYM`, `XYZM`), and
65
+ concrete subtype objects (`Point`, `LineString`, `Polygon`,
66
+ `MultiPoint`, `MultiLineString`, `MultiPolygon`,
67
+ `GeometryCollection`).
68
+ - **Subtype-specific accessors** — `x` / `y` / `z` on points
69
+ (`longitude` / `latitude` / `height` on `geography`), `points` on
70
+ line strings, `exterior` / `interiors` on polygons, etc.
71
+ - **Measurement fields** — `length`, `area`, `perimeter`, computed
72
+ geodesically from GeoJSON on the server.
73
+ - **Transformation fields** — `centroid`, `bbox`, `numPoints`.
74
+ - **Per-column spatial filters** — every PostGIS topological
75
+ predicate (`intersects`, `contains`, `within`, `dwithin`, …) and
76
+ every bounding-box operator (`bboxIntersects2D`, `bboxContains`,
77
+ `bboxLeftOf`, …) wired into the generated `where:` shape.
78
+ - **Aggregate fields** — `stExtent`, `stUnion`, `stCollect`,
79
+ `stConvexHull` exposed on every aggregate type for a geometry
80
+ column.
81
+ - **Spatial relations** — a `@spatialRelation` smart tag that
82
+ declares cross-table spatial joins as first-class relations (ORM +
83
+ GraphQL), backed by PostGIS predicates and GIST indexes.
84
+ - **Auto-detects PostGIS** in any schema (not just `public`) and
85
+ **degrades gracefully** when the extension isn't installed.
86
+
87
+ ## GeoJSON scalar and typed geometry columns
88
+
89
+ A `geometry` / `geography` column is exposed as a typed GraphQL object
90
+ with a `geojson` field carrying the GeoJSON payload. You select it the
91
+ same way you select any nested object:
92
+
93
+ ```ts
94
+ // Read a location column as GeoJSON through the ORM
95
+ const result = await orm.location
96
+ .findMany({
97
+ select: { name: true, geom: { select: { geojson: true } } },
98
+ where: { name: { equalTo: 'Central Park Cafe' } },
99
+ })
100
+ .execute();
101
+ ```
102
+
103
+ Input values (mutations, filters) accept GeoJSON directly — any of
104
+ `Point`, `LineString`, `Polygon`, `MultiPoint`, `MultiLineString`,
105
+ `MultiPolygon`, or `GeometryCollection`.
106
+
107
+ ## Dimension-aware interfaces and subtype fields
108
+
109
+ Each concrete subtype is its own GraphQL object with fields that make
110
+ sense for that subtype:
111
+
112
+ | Subtype | Notable fields |
113
+ |---------------------|------------------------------------------------------|
114
+ | `Point` | `x` / `y` / `z` (or `longitude` / `latitude` / `height` on `geography`) |
115
+ | `LineString` | `points: [Point!]` |
116
+ | `Polygon` | `exterior: LineString`, `interiors: [LineString!]` |
117
+ | `MultiPoint` | `points: [Point!]` |
118
+ | `MultiLineString` | `lineStrings: [LineString!]` |
119
+ | `MultiPolygon` | `polygons: [Polygon!]` |
120
+ | `GeometryCollection`| `geometries: [Geometry!]` |
121
+
122
+ On top of those, every geometry type also exposes the `XY` / `XYZ` /
123
+ `XYM` / `XYZM` dimension interfaces so a client can ask for
124
+ coordinates without branching on the specific subtype.
125
+
126
+ ```graphql
127
+ # Example GraphQL selection on a Polygon column
128
+ {
129
+ counties {
130
+ nodes {
131
+ name
132
+ geom {
133
+ geojson
134
+ exterior {
135
+ points { x y }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ ## Measurement fields (`length`, `area`, `perimeter`)
144
+
145
+ Subtype-appropriate measurement fields are added automatically, using
146
+ geodesic math on the GeoJSON payload (Haversine for distance,
147
+ spherical excess for area, WGS84 / SRID 4326 assumed):
148
+
149
+ | Subtype | Fields added |
150
+ |-------------------------------------------------|-------------------------|
151
+ | `LineString`, `MultiLineString` | `length` |
152
+ | `Polygon`, `MultiPolygon` | `area`, `perimeter` |
153
+
154
+ Values are `Float` in meters (length / perimeter) and square meters
155
+ (area).
156
+
157
+ ```graphql
158
+ {
159
+ counties {
160
+ nodes {
161
+ name
162
+ geom { area perimeter }
163
+ }
164
+ }
165
+ routes {
166
+ nodes {
167
+ id
168
+ path { length }
169
+ }
170
+ }
171
+ }
172
+ ```
173
+
174
+ For exact server-side PostGIS measurements (e.g. `ST_Area` with a
175
+ specific SRID projection), define a computed column in SQL — these
176
+ fields are client-facing conveniences, not a replacement for
177
+ projection-aware analytics.
178
+
179
+ ## Transformation fields (`centroid`, `bbox`, `numPoints`)
180
+
181
+ Every geometry object also gets three lightweight transformation
182
+ fields:
183
+
184
+ - `centroid: [Float!]` — coordinate-mean centroid.
185
+ - `bbox: [Float!]` — `[minX, minY, maxX, maxY]` bounding box.
186
+ - `numPoints: Int!` — total coordinate count.
187
+
188
+ ```graphql
189
+ {
190
+ parks {
191
+ nodes {
192
+ name
193
+ geom { centroid bbox numPoints }
194
+ }
195
+ }
196
+ }
197
+ ```
198
+
199
+ For `ST_Transform` / `ST_Buffer` / `ST_Simplify` / `ST_MakeValid`,
200
+ which all take parameters, declare a custom SQL function or computed
201
+ column — the object-level transformation fields intentionally stick
202
+ to parameter-free helpers.
203
+
204
+ ## Per-column spatial filters
205
+
206
+ Every PostGIS predicate is registered as a filter operator on the
207
+ column's `where:` entry, both for `geometry` and `geography` codecs:
208
+
209
+ - Topological: `intersects`, `contains`, `containsProperly`, `within`,
210
+ `covers`, `coveredBy`, `touches`, `crosses`, `disjoint`, `overlaps`,
211
+ `equals`, `orderingEquals`.
212
+ - Distance: `dwithin` (parametric).
213
+ - 2D / ND bounding-box: `bboxIntersects2D`, `bboxIntersectsND`,
214
+ `bboxContains`, `bboxEquals`.
215
+ - Directional bounding-box: `bboxLeftOf`, `bboxRightOf`, `bboxAbove`,
216
+ `bboxBelow`, `bboxOverlapsOrLeftOf`, `bboxOverlapsOrRightOf`,
217
+ `bboxOverlapsOrAbove`, `bboxOverlapsOrBelow`.
218
+
219
+ All of them take GeoJSON as input — the plugin wraps the value with
220
+ `ST_GeomFromGeoJSON(...)::<codec>` before it hits PostgreSQL, so
221
+ `Point`, `LineString`, `Polygon`, `MultiPoint`, `MultiLineString`,
222
+ `MultiPolygon`, and `GeometryCollection` inputs all work uniformly.
223
+
224
+ ```ts
225
+ // Cities whose location is inside a polygon
226
+ const inBayArea = await orm.citiesGeom
227
+ .findMany({
228
+ select: { id: true, name: true },
229
+ where: { loc: { intersects: BAY_AREA_POLYGON } },
230
+ })
231
+ .execute();
232
+
233
+ // Cities whose bbox sits strictly west of a reference point
234
+ const westOfCentral = await orm.citiesGeom
235
+ .findMany({
236
+ select: { id: true, name: true },
237
+ where: { loc: { bboxLeftOf: { type: 'Point', coordinates: [-100.0, 37.77] } } },
238
+ })
239
+ .execute();
240
+ ```
241
+
242
+ ## PostGIS aggregate fields
243
+
244
+ On every aggregate type for a geometry / geography column, the plugin
245
+ adds four SQL-level aggregate fields that run in-database:
246
+
247
+ - `stExtent` — `ST_Extent(...)` — bounding box of all rows as a
248
+ GeoJSON Polygon.
249
+ - `stUnion` — `ST_Union(...)` — union of all rows as GeoJSON.
250
+ - `stCollect` — `ST_Collect(...)` — collect into a
251
+ `GeometryCollection`.
252
+ - `stConvexHull` — `ST_ConvexHull(ST_Collect(...))` — convex hull of
253
+ all rows as a GeoJSON Polygon.
254
+
255
+ ```graphql
256
+ {
257
+ citiesGeoms {
258
+ aggregates {
259
+ stExtent { loc { geojson } }
260
+ stConvexHull { loc { geojson } }
261
+ }
262
+ }
263
+ }
264
+ ```
265
+
266
+ ## Spatial relations (`@spatialRelation`)
267
+
268
+ Spatial relations are the plugin's cross-table feature: a way to
269
+ declare, directly on a database column, that two tables are related
270
+ *spatially* — "clinics inside a county", "parcels touching a road",
271
+ "events within 5 km of a user" — and get a first-class relation in
272
+ the generated ORM and GraphQL schema for free.
273
+
274
+ ### Why a dedicated primitive
275
+
276
+ Without this, spatial joins from an app usually devolve into shipping
277
+ GeoJSON across the wire: every clinic as GeoJSON, every county
278
+ polygon as GeoJSON, a point-in-polygon loop on the client. An
279
+ auto-generated ORM can't do better on its own — it sees a `geometry`
280
+ column and stops there. Foreign keys describe equality; nothing
281
+ describes *containment* or *proximity*.
282
+
283
+ A `@spatialRelation` tag declares that `clinics.location` is
284
+ "within" `counties.geom`, and the generated schema + ORM gain a
285
+ first-class `where: { county: { some: { … } } }` shape that runs the
286
+ join server-side, in one SQL query, using PostGIS and a GIST index.
287
+ No GeoJSON on the wire; the relation composes with the rest of your
288
+ `where:` the same way a foreign-key relation would.
289
+
290
+ ### Declaring a relation
291
+
292
+ Put the tag on the owning geometry / geography column:
293
+
294
+ ```sql
295
+ COMMENT ON COLUMN telemedicine_clinics.location IS
296
+ E'@spatialRelation county counties.geom st_within';
297
+ ```
298
+
299
+ Tag grammar:
300
+
301
+ ```
302
+ @spatialRelation <relationName> <targetRef> <operator> [<paramName>]
303
+ ```
304
+
305
+ - `<relationName>` — user-chosen name for the new field on the
306
+ owner's `where` input. Must match `/^[A-Za-z_][A-Za-z0-9_]*$/`.
307
+ - `<targetRef>` — `table.column` (defaults to the owning column's
308
+ schema) or `schema.table.column`.
309
+ - `<operator>` — one of the eight PG-native snake_case tokens listed
310
+ below.
311
+ - `<paramName>` — required if the operator is parametric. Today that
312
+ is only `st_dwithin` (use `distance`).
313
+
314
+ Both sides must be `geometry` or `geography`, and share the **same**
315
+ codec — mixing is rejected at schema build.
316
+
317
+ Stack multiple relations on one column by separating tags with `\n`:
318
+
319
+ ```sql
320
+ COMMENT ON COLUMN telemedicine_clinics.location IS
321
+ E'@spatialRelation county counties.geom st_within\n'
322
+ '@spatialRelation intersectingCounty counties.geom st_intersects\n'
323
+ '@spatialRelation coveringCounty counties.geom st_coveredby\n'
324
+ '@spatialRelation nearbyClinic telemedicine_clinics.location st_dwithin distance';
325
+ ```
326
+
327
+ ### Operator reference
328
+
329
+ | Tag operator | PostGIS function | Parametric? | Symmetric? | Typical use |
330
+ |----------------------|-----------------------|----------------|-----------------------|--------------------------------------------------|
331
+ | `st_contains` | `ST_Contains(A, B)` | no | **no** (A contains B) | polygon containing a point / line / polygon |
332
+ | `st_within` | `ST_Within(A, B)` | no | **no** (A within B) | point-in-polygon, line-in-polygon |
333
+ | `st_covers` | `ST_Covers(A, B)` | no | **no** | like `st_contains`, boundary-inclusive |
334
+ | `st_coveredby` | `ST_CoveredBy(A, B)` | no | **no** | dual of `st_covers` |
335
+ | `st_intersects` | `ST_Intersects(A, B)` | no | yes | any overlap at all |
336
+ | `st_equals` | `ST_Equals(A, B)` | no | yes | exact geometry match |
337
+ | `st_bbox_intersects` | `A && B` (infix) | no | yes | fast bounding-box prefilter |
338
+ | `st_dwithin` | `ST_DWithin(A, B, d)` | **yes** (`d`) | yes | radius / proximity search |
339
+
340
+ The tag reads left-to-right as **"owner op target"**, and the emitted
341
+ SQL is exactly `ST_<op>(owner_col, target_col[, distance])`. For
342
+ directional operators (`st_within`, `st_contains`, `st_covers`,
343
+ `st_coveredby`), flipping the two columns inverts the result set; put
344
+ the relation on the column whose type makes the sentence true.
345
+
346
+ ### Using a spatial relation from the ORM
347
+
348
+ Every 2-argument relation exposes `some` / `every` / `none` against
349
+ the target table's full `where` input:
350
+
351
+ ```ts
352
+ // "Clinics inside LA County" — st_within, one SQL query, no GeoJSON on the wire.
353
+ await orm.telemedicineClinic
354
+ .findMany({
355
+ select: { id: true, name: true },
356
+ where: { county: { some: { name: { equalTo: 'LA County' } } } },
357
+ })
358
+ .execute();
359
+
360
+ // "Clinics NOT in NYC County" — negation via `none`.
361
+ await orm.telemedicineClinic
362
+ .findMany({
363
+ select: { id: true },
364
+ where: { county: { none: { name: { equalTo: 'NYC County' } } } },
365
+ })
366
+ .execute();
367
+
368
+ // "Any clinic that sits inside at least one county" — empty inner
369
+ // clause still excludes points that fall outside every county.
370
+ await orm.telemedicineClinic
371
+ .findMany({
372
+ select: { id: true, name: true },
373
+ where: { county: { some: {} } },
374
+ })
375
+ .execute();
376
+ ```
377
+
378
+ Parametric relations (today: `st_dwithin`) add a required `distance`
379
+ field alongside `some` / `every` / `none`:
380
+
381
+ ```ts
382
+ // "Clinics within 10 SRID units of any cardiology clinic" — self-relation
383
+ // with parametric distance; a row never matches itself.
384
+ await orm.telemedicineClinic
385
+ .findMany({
386
+ select: { id: true, name: true },
387
+ where: {
388
+ nearbyClinic: {
389
+ distance: 10.0,
390
+ some: { specialty: { equalTo: 'cardiology' } },
391
+ },
392
+ },
393
+ })
394
+ .execute();
395
+ ```
396
+
397
+ ### Using a spatial relation from GraphQL
398
+
399
+ The same tree, same field names — just under `where:` on the
400
+ connection argument:
401
+
402
+ ```graphql
403
+ {
404
+ telemedicineClinics(
405
+ where: { county: { some: { name: { equalTo: "Bay County" } } } }
406
+ ) {
407
+ nodes { id name }
408
+ }
409
+ }
410
+ ```
411
+
412
+ ### Composition
413
+
414
+ Spatial relations live in the same `where:` tree as every scalar
415
+ predicate and compose identically:
416
+
417
+ ```ts
418
+ // Bay County clinics that are cardiology
419
+ where: {
420
+ and: [
421
+ { county: { some: { name: { equalTo: 'Bay County' } } } },
422
+ { specialty: { equalTo: 'cardiology' } },
423
+ ],
424
+ }
425
+
426
+ // Bay County clinics OR the one named "LA Pediatrics"
427
+ where: {
428
+ or: [
429
+ { county: { some: { name: { equalTo: 'Bay County' } } } },
430
+ { name: { equalTo: 'LA Pediatrics' } },
431
+ ],
432
+ }
433
+
434
+ // Clinics NOT in Bay County
435
+ where: {
436
+ not: { county: { some: { name: { equalTo: 'Bay County' } } } },
437
+ }
438
+ ```
439
+
440
+ ### Self-relations and self-exclusion
441
+
442
+ When the owner and target columns are the same column, the plugin
443
+ emits a self-exclusion predicate so a row never matches itself:
444
+
445
+ - Single-column primary key: `other.<pk> <> self.<pk>`.
446
+ - Composite primary key: `(other.a, other.b) IS DISTINCT FROM (self.a, self.b)`.
447
+ - Tables without a primary key are rejected at schema build.
448
+
449
+ One consequence: with `st_dwithin`, a self-relation at `distance: 0`
450
+ matches zero rows, because the only candidate at distance 0 is the
451
+ row itself — and it is excluded.
452
+
453
+ ### Generated SQL shape
454
+
455
+ ```sql
456
+ SELECT ...
457
+ FROM <owner_table> self
458
+ WHERE EXISTS (
459
+ SELECT 1
460
+ FROM <target_table> other
461
+ WHERE ST_<op>(self.<owner_col>, other.<target_col>[, <distance>])
462
+ AND <self-exclusion for self-relations>
463
+ AND <nested some/every/none conditions>
464
+ );
465
+ ```
466
+
467
+ The `EXISTS` sits inside the owner's generated `where` input, so it
468
+ composes cleanly with pagination, ordering, and the rest of the outer
469
+ plan. `st_bbox_intersects` compiles to infix `&&` rather than a
470
+ function call. PostGIS functions are called with whichever schema
471
+ PostGIS is installed in, so non-`public` installs work without extra
472
+ configuration.
473
+
474
+ ### Indexing
475
+
476
+ Spatial predicates without a GIST index fall back to sequential scans,
477
+ which is almost never what you want. The plugin checks your target
478
+ columns at schema-build time and emits a non-fatal warning when a
479
+ GIST index is missing, including the recommended `CREATE INDEX …
480
+ USING GIST(...)` in the warning text:
481
+
482
+ ```sql
483
+ CREATE INDEX ON telemedicine_clinics USING GIST(location);
484
+ CREATE INDEX ON counties USING GIST(geom);
485
+ ```
36
486
 
37
- - GeoJSON scalar type for input/output
38
- - GraphQL interfaces for geometry and geography base types
39
- - Dimension-aware interfaces (XY, XYZ, XYM, XYZM)
40
- - Concrete types for all geometry subtypes: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection
41
- - Subtype-specific fields (x/y/z for Points, points for LineStrings, exterior/interiors for Polygons, etc.)
42
- - Geography-aware field naming (longitude/latitude/height instead of x/y/z)
43
- - Graceful degradation when PostGIS is not installed
487
+ If a particular column is a known exception (e.g. a small prototype
488
+ table), set `@spatialRelationSkipIndexCheck` on that column to
489
+ suppress the warning.
490
+
491
+ ### `geometry` vs `geography`
492
+
493
+ Pick one side of a relation and stick with it — mixing codecs across
494
+ the two sides is rejected at schema build. `geography` distances are
495
+ always meters; `geometry` distances follow the SRID's native units
496
+ (degrees for SRID 4326, which is rarely what you want for radius
497
+ searches). If you need meter-based proximity on a `geometry` column,
498
+ cast on ingest (`::geography`) rather than mixing codecs across a
499
+ single relation.
500
+
501
+ ### FAQ
502
+
503
+ - **"Why doesn't `some: {}` return every row?"** — because `some`
504
+ means "at least one related target row exists". Rows whose column
505
+ has no match on the other side are correctly excluded.
506
+ - **"Why does `distance: 0` on a self-relation return nothing?"** —
507
+ the self-exclusion predicate removes the row's match with itself,
508
+ so at distance 0 no candidates remain.
509
+ - **"Can I reuse a `relationName` across tables?"** — yes; uniqueness
510
+ is scoped to the owning table.
511
+ - **"Can I declare the relation from the polygon side instead of the
512
+ point side?"** — yes. Flip owner and target and use the inverse
513
+ operator (`st_contains` in place of `st_within`). Same rows, same
514
+ SQL, different `where` location.
515
+ - **"Does this work with PostGIS installed in a non-`public`
516
+ schema?"** — yes.
517
+ - **"Can I use a spatial relation in `orderBy` or on a connection
518
+ field?"** — no; it's a where-only construct. Use the measurement /
519
+ transformation fields for values you want to sort on.
520
+
521
+ ## Graceful degradation
522
+
523
+ If the `postgis` extension isn't installed in the target database,
524
+ the plugin detects that at schema-build time and skips type, filter,
525
+ aggregate, and spatial-relation registration instead of breaking the
526
+ build. Turning PostGIS on later only requires restarting the server
527
+ (or invalidating the schema cache) — no config change.
44
528
 
45
529
  ## License
46
530
 
package/esm/index.d.ts CHANGED
@@ -21,6 +21,8 @@ export { PostgisGeometryFieldsPlugin } from './plugins/geometry-fields';
21
21
  export { PostgisMeasurementFieldsPlugin } from './plugins/measurement-fields';
22
22
  export { PostgisTransformationFieldsPlugin } from './plugins/transformation-functions';
23
23
  export { PostgisAggregatePlugin } from './plugins/aggregate-functions';
24
+ export { PostgisSpatialRelationsPlugin, OPERATOR_REGISTRY, parseSpatialRelationTag, collectSpatialRelations, } from './plugins/spatial-relations';
25
+ export type { SpatialOperatorRegistration, SpatialRelationInfo, } from './plugins/spatial-relations';
24
26
  export { createPostgisOperatorFactory } from './plugins/connection-filter-operators';
25
27
  export { createWithinDistanceOperatorFactory } from './plugins/within-distance-operator';
26
28
  export { GisSubtype, SUBTYPE_STRING_BY_SUBTYPE, GIS_SUBTYPE_NAME, CONCRETE_SUBTYPES } from './constants';
package/esm/index.js CHANGED
@@ -23,6 +23,7 @@ export { PostgisGeometryFieldsPlugin } from './plugins/geometry-fields';
23
23
  export { PostgisMeasurementFieldsPlugin } from './plugins/measurement-fields';
24
24
  export { PostgisTransformationFieldsPlugin } from './plugins/transformation-functions';
25
25
  export { PostgisAggregatePlugin } from './plugins/aggregate-functions';
26
+ export { PostgisSpatialRelationsPlugin, OPERATOR_REGISTRY, parseSpatialRelationTag, collectSpatialRelations, } from './plugins/spatial-relations';
26
27
  // Connection filter operator factories (spatial operators for graphile-connection-filter)
27
28
  export { createPostgisOperatorFactory } from './plugins/connection-filter-operators';
28
29
  export { createWithinDistanceOperatorFactory } from './plugins/within-distance-operator';
@@ -207,6 +207,8 @@ export function createPostgisOperatorFactory() {
207
207
  }
208
208
  const { inflection } = build;
209
209
  const { schemaName, geometryCodec, geographyCodec } = postgisInfo;
210
+ const sqlGeomFromGeoJSON = sql.identifier(schemaName, 'st_geomfromgeojson');
211
+ const sqlGeographyType = sql.identifier(schemaName, 'geography');
210
212
  // Collect all GQL type names for geometry and geography
211
213
  const gqlTypeNamesByBase = {
212
214
  geometry: [],
@@ -239,6 +241,7 @@ export function createPostgisOperatorFactory() {
239
241
  typeNames: gqlTypeNamesByBase[baseType],
240
242
  operatorName,
241
243
  description,
244
+ baseType: baseType,
242
245
  resolve: (i, v) => sql.fragment `${sqlGisFunction}(${i}, ${v})`
243
246
  });
244
247
  }
@@ -252,6 +255,7 @@ export function createPostgisOperatorFactory() {
252
255
  typeNames: gqlTypeNamesByBase[baseType],
253
256
  operatorName,
254
257
  description,
258
+ baseType: baseType,
255
259
  resolve: (i, v) => buildOperatorExpr(capturedOp, i, v)
256
260
  });
257
261
  }
@@ -261,8 +265,21 @@ export function createPostgisOperatorFactory() {
261
265
  // Convert to ConnectionFilterOperatorRegistration format.
262
266
  // Each InternalSpec may target multiple type names; we expand each
263
267
  // into individual registrations keyed by typeName.
268
+ //
269
+ // The default operatorApply pipeline binds the filter value as a raw
270
+ // text parameter cast to the column codec's sqlType (geometry /
271
+ // geography). PostgreSQL's geometry_in / geography_in parsers reject
272
+ // GeoJSON text, so we must wrap the input with ST_GeomFromGeoJSON
273
+ // ourselves — see within-distance-operator.ts for the pattern.
274
+ //
275
+ // We disable the default bind via `resolveSqlValue: () => sql.null`
276
+ // and construct the geometry value from `input` inside resolve(),
277
+ // mirroring the ST_DWithin implementation.
264
278
  const registrations = [];
265
279
  for (const spec of allSpecs) {
280
+ const geographyCast = spec.baseType === 'geography'
281
+ ? sql.fragment `::${sqlGeographyType}`
282
+ : sql.fragment ``;
266
283
  for (const typeName of spec.typeNames) {
267
284
  registrations.push({
268
285
  typeNames: typeName,
@@ -270,8 +287,11 @@ export function createPostgisOperatorFactory() {
270
287
  spec: {
271
288
  description: spec.description,
272
289
  resolveType: (fieldType) => fieldType,
273
- resolve(sqlIdentifier, sqlValue, _input, _$where, _details) {
274
- return spec.resolve(sqlIdentifier, sqlValue);
290
+ resolveSqlValue: () => sql.null,
291
+ resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
292
+ const geoJsonStr = sql.value(JSON.stringify(input));
293
+ const geomSql = sql.fragment `${sqlGeomFromGeoJSON}(${geoJsonStr}::text)${geographyCast}`;
294
+ return spec.resolve(sqlIdentifier, geomSql);
275
295
  }
276
296
  },
277
297
  });