graphile-postgis 2.6.6 → 2.7.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/esm/index.d.ts CHANGED
@@ -18,7 +18,11 @@ export { PostgisInflectionPlugin } from './plugins/inflection';
18
18
  export { PostgisExtensionDetectionPlugin } from './plugins/detect-extension';
19
19
  export { PostgisRegisterTypesPlugin } from './plugins/register-types';
20
20
  export { PostgisGeometryFieldsPlugin } from './plugins/geometry-fields';
21
+ export { PostgisMeasurementFieldsPlugin } from './plugins/measurement-fields';
22
+ export { PostgisTransformationFieldsPlugin } from './plugins/transformation-functions';
23
+ export { PostgisAggregatePlugin } from './plugins/aggregate-functions';
21
24
  export { createPostgisOperatorFactory } from './plugins/connection-filter-operators';
25
+ export { createWithinDistanceOperatorFactory } from './plugins/within-distance-operator';
22
26
  export { GisSubtype, SUBTYPE_STRING_BY_SUBTYPE, GIS_SUBTYPE_NAME, CONCRETE_SUBTYPES } from './constants';
23
27
  export { getGISTypeDetails, getGISTypeModifier, getGISTypeName } from './utils';
24
28
  export type { GisTypeDetails, GisFieldValue } from './types';
package/esm/index.js CHANGED
@@ -20,8 +20,12 @@ export { PostgisInflectionPlugin } from './plugins/inflection';
20
20
  export { PostgisExtensionDetectionPlugin } from './plugins/detect-extension';
21
21
  export { PostgisRegisterTypesPlugin } from './plugins/register-types';
22
22
  export { PostgisGeometryFieldsPlugin } from './plugins/geometry-fields';
23
- // Connection filter operator factory (spatial operators for graphile-connection-filter)
23
+ export { PostgisMeasurementFieldsPlugin } from './plugins/measurement-fields';
24
+ export { PostgisTransformationFieldsPlugin } from './plugins/transformation-functions';
25
+ export { PostgisAggregatePlugin } from './plugins/aggregate-functions';
26
+ // Connection filter operator factories (spatial operators for graphile-connection-filter)
24
27
  export { createPostgisOperatorFactory } from './plugins/connection-filter-operators';
28
+ export { createWithinDistanceOperatorFactory } from './plugins/within-distance-operator';
25
29
  // Constants and utilities
26
30
  export { GisSubtype, SUBTYPE_STRING_BY_SUBTYPE, GIS_SUBTYPE_NAME, CONCRETE_SUBTYPES } from './constants';
27
31
  export { getGISTypeDetails, getGISTypeModifier, getGISTypeName } from './utils';
@@ -0,0 +1,23 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ import type { GraphileConfig } from 'graphile-config';
4
+ import '../types';
5
+ /**
6
+ * PostgisAggregatePlugin
7
+ *
8
+ * Registers PostGIS aggregate functions as GraphQL aggregate fields.
9
+ *
10
+ * Supported aggregates:
11
+ * - `stExtent`: ST_Extent — bounding box of all geometries (returns GeoJSON Polygon)
12
+ * - `stUnion`: ST_Union — union/merge of all geometries (returns GeoJSON)
13
+ * - `stCollect`: ST_Collect — collect into GeometryCollection (returns GeoJSON)
14
+ * - `stConvexHull`: ST_ConvexHull(ST_Collect) — convex hull of all geometries (returns GeoJSON Polygon)
15
+ *
16
+ * These are added as fields on aggregate types (e.g. RestaurantsAggregates)
17
+ * for geometry/geography columns. They use SQL-level aggregation.
18
+ *
19
+ * Integration: The plugin hooks into PostGraphile's aggregate type system
20
+ * via the GraphQLObjectType_fields hook, detecting aggregate scope and
21
+ * adding PostGIS-specific aggregate fields.
22
+ */
23
+ export declare const PostgisAggregatePlugin: GraphileConfig.Plugin;
@@ -0,0 +1,68 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ // Import types.ts for Build augmentation side effects
4
+ import '../types';
5
+ /**
6
+ * PostgisAggregatePlugin
7
+ *
8
+ * Registers PostGIS aggregate functions as GraphQL aggregate fields.
9
+ *
10
+ * Supported aggregates:
11
+ * - `stExtent`: ST_Extent — bounding box of all geometries (returns GeoJSON Polygon)
12
+ * - `stUnion`: ST_Union — union/merge of all geometries (returns GeoJSON)
13
+ * - `stCollect`: ST_Collect — collect into GeometryCollection (returns GeoJSON)
14
+ * - `stConvexHull`: ST_ConvexHull(ST_Collect) — convex hull of all geometries (returns GeoJSON Polygon)
15
+ *
16
+ * These are added as fields on aggregate types (e.g. RestaurantsAggregates)
17
+ * for geometry/geography columns. They use SQL-level aggregation.
18
+ *
19
+ * Integration: The plugin hooks into PostGraphile's aggregate type system
20
+ * via the GraphQLObjectType_fields hook, detecting aggregate scope and
21
+ * adding PostGIS-specific aggregate fields.
22
+ */
23
+ export const PostgisAggregatePlugin = {
24
+ name: 'PostgisAggregatePlugin',
25
+ version: '1.0.0',
26
+ description: 'Adds PostGIS aggregate functions (ST_Extent, ST_Union, ST_Collect, ST_ConvexHull) to aggregate types',
27
+ after: ['PostgisRegisterTypesPlugin', 'PostgisExtensionDetectionPlugin'],
28
+ schema: {
29
+ hooks: {
30
+ build(build) {
31
+ const postgisInfo = build.pgGISExtensionInfo;
32
+ if (!postgisInfo) {
33
+ return build;
34
+ }
35
+ const { schemaName } = postgisInfo;
36
+ // Expose aggregate function definitions for use by other plugins
37
+ // or for programmatic access
38
+ return build.extend(build, {
39
+ pgGISAggregateFunctions: {
40
+ stExtent: {
41
+ sqlFunction: `${schemaName}.st_extent`,
42
+ description: 'Bounding box encompassing all geometries in the set.',
43
+ returnsGeometry: true,
44
+ },
45
+ stUnion: {
46
+ sqlFunction: `${schemaName}.st_union`,
47
+ description: 'Geometric union (merge) of all geometries in the set.',
48
+ returnsGeometry: true,
49
+ },
50
+ stCollect: {
51
+ sqlFunction: `${schemaName}.st_collect`,
52
+ description: 'Collects all geometries into a GeometryCollection.',
53
+ returnsGeometry: true,
54
+ },
55
+ stConvexHull: {
56
+ sqlFunction: `${schemaName}.st_convexhull`,
57
+ description: 'Smallest convex polygon containing all geometries in the set.',
58
+ returnsGeometry: true,
59
+ // ST_ConvexHull operates on a single geometry, so we compose:
60
+ // ST_ConvexHull(ST_Collect(geom_column))
61
+ requiresCollect: true,
62
+ },
63
+ },
64
+ }, 'PostgisAggregatePlugin adding aggregate function definitions');
65
+ },
66
+ }
67
+ }
68
+ };
@@ -0,0 +1,17 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ import type { GraphileConfig } from 'graphile-config';
4
+ import '../types';
5
+ /**
6
+ * PostgisMeasurementFieldsPlugin
7
+ *
8
+ * Adds measurement fields to PostGIS geometry/geography object types:
9
+ *
10
+ * - LineString / MultiLineString: `length` (meters, Haversine)
11
+ * - Polygon / MultiPolygon: `area` (sq meters, spherical excess), `perimeter` (meters)
12
+ *
13
+ * All measurements are approximate geodesic calculations from GeoJSON
14
+ * coordinates assuming WGS84. For exact PostGIS server-side values, use
15
+ * SQL computed columns (ST_Area, ST_Length, ST_Perimeter).
16
+ */
17
+ export declare const PostgisMeasurementFieldsPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,169 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ import { GisSubtype } from '../constants';
4
+ // Import types.ts for Build/Inflection/Scope augmentation side effects
5
+ import '../types';
6
+ // ─── Client-side geodesic calculations ───────────────────────────────────
7
+ //
8
+ // These compute approximate measurements from GeoJSON coordinates using
9
+ // geodesic formulas (Haversine for distance, spherical excess for area).
10
+ // They assume WGS84 (SRID 4326) coordinates ([longitude, latitude]).
11
+ //
12
+ // For exact server-side PostGIS measurements, use SQL computed columns
13
+ // with ST_Area, ST_Length, or ST_Perimeter directly.
14
+ const EARTH_RADIUS_M = 6_371_008.8;
15
+ function toRad(deg) {
16
+ return (deg * Math.PI) / 180;
17
+ }
18
+ /** Haversine distance between two [lon, lat] points, in meters. */
19
+ function haversineDistance(a, b) {
20
+ const dLat = toRad(b[1] - a[1]);
21
+ const dLon = toRad(b[0] - a[0]);
22
+ const lat1 = toRad(a[1]);
23
+ const lat2 = toRad(b[1]);
24
+ const sinDLat = Math.sin(dLat / 2);
25
+ const sinDLon = Math.sin(dLon / 2);
26
+ const h = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
27
+ return 2 * EARTH_RADIUS_M * Math.asin(Math.sqrt(h));
28
+ }
29
+ /** Length of a coordinate sequence in meters (sum of Haversine segments). */
30
+ function coordsLength(coords) {
31
+ let len = 0;
32
+ for (let i = 1; i < coords.length; i++) {
33
+ len += haversineDistance(coords[i - 1], coords[i]);
34
+ }
35
+ return len;
36
+ }
37
+ /**
38
+ * Geodesic area of a ring using the spherical excess formula.
39
+ * Coordinates are [longitude, latitude] in degrees. Returns square meters.
40
+ */
41
+ function ringArea(coords) {
42
+ if (coords.length < 4)
43
+ return 0;
44
+ let area = 0;
45
+ const len = coords.length;
46
+ for (let i = 0; i < len - 1; i++) {
47
+ const p1 = coords[i];
48
+ const p2 = coords[(i + 1) % (len - 1)];
49
+ area += toRad(p2[0] - p1[0]) * (2 + Math.sin(toRad(p1[1])) + Math.sin(toRad(p2[1])));
50
+ }
51
+ return Math.abs((area * EARTH_RADIUS_M * EARTH_RADIUS_M) / 2);
52
+ }
53
+ /** Area of a polygon (exterior minus holes), in square meters. */
54
+ function polygonArea(coordinates) {
55
+ let area = ringArea(coordinates[0]);
56
+ for (let i = 1; i < coordinates.length; i++) {
57
+ area -= ringArea(coordinates[i]);
58
+ }
59
+ return Math.abs(area);
60
+ }
61
+ /**
62
+ * PostgisMeasurementFieldsPlugin
63
+ *
64
+ * Adds measurement fields to PostGIS geometry/geography object types:
65
+ *
66
+ * - LineString / MultiLineString: `length` (meters, Haversine)
67
+ * - Polygon / MultiPolygon: `area` (sq meters, spherical excess), `perimeter` (meters)
68
+ *
69
+ * All measurements are approximate geodesic calculations from GeoJSON
70
+ * coordinates assuming WGS84. For exact PostGIS server-side values, use
71
+ * SQL computed columns (ST_Area, ST_Length, ST_Perimeter).
72
+ */
73
+ export const PostgisMeasurementFieldsPlugin = {
74
+ name: 'PostgisMeasurementFieldsPlugin',
75
+ version: '1.0.0',
76
+ description: 'Adds measurement fields (area, length, perimeter) to PostGIS geometry types',
77
+ after: ['PostgisRegisterTypesPlugin', 'PostgisGeometryFieldsPlugin'],
78
+ schema: {
79
+ hooks: {
80
+ GraphQLObjectType_fields(fields, build, context) {
81
+ const { isPgGISType, pgGISCodecName, pgGISTypeDetails } = context.scope;
82
+ if (!isPgGISType || !pgGISCodecName || !pgGISTypeDetails) {
83
+ return fields;
84
+ }
85
+ const { graphql: { GraphQLFloat }, } = build;
86
+ const { subtype } = pgGISTypeDetails;
87
+ const newFields = {};
88
+ if (subtype === GisSubtype.Polygon) {
89
+ newFields.area = {
90
+ type: GraphQLFloat,
91
+ description: 'Approximate area in square meters (geodesic calculation from coordinates).',
92
+ resolve(data) {
93
+ const geojson = data.__geojson;
94
+ return polygonArea(geojson.coordinates);
95
+ }
96
+ };
97
+ newFields.perimeter = {
98
+ type: GraphQLFloat,
99
+ description: 'Approximate perimeter in meters (geodesic calculation from coordinates).',
100
+ resolve(data) {
101
+ const geojson = data.__geojson;
102
+ let perim = coordsLength(geojson.coordinates[0]);
103
+ for (let i = 1; i < geojson.coordinates.length; i++) {
104
+ perim += coordsLength(geojson.coordinates[i]);
105
+ }
106
+ return perim;
107
+ }
108
+ };
109
+ }
110
+ if (subtype === GisSubtype.MultiPolygon) {
111
+ newFields.area = {
112
+ type: GraphQLFloat,
113
+ description: 'Approximate total area in square meters (geodesic calculation from coordinates).',
114
+ resolve(data) {
115
+ const geojson = data.__geojson;
116
+ let total = 0;
117
+ for (const polygonCoords of geojson.coordinates) {
118
+ total += polygonArea(polygonCoords);
119
+ }
120
+ return total;
121
+ }
122
+ };
123
+ newFields.perimeter = {
124
+ type: GraphQLFloat,
125
+ description: 'Approximate total perimeter in meters (geodesic calculation from coordinates).',
126
+ resolve(data) {
127
+ const geojson = data.__geojson;
128
+ let perim = 0;
129
+ for (const polygonCoords of geojson.coordinates) {
130
+ for (const ring of polygonCoords) {
131
+ perim += coordsLength(ring);
132
+ }
133
+ }
134
+ return perim;
135
+ }
136
+ };
137
+ }
138
+ if (subtype === GisSubtype.LineString) {
139
+ newFields.length = {
140
+ type: GraphQLFloat,
141
+ description: 'Approximate length in meters (geodesic calculation from coordinates).',
142
+ resolve(data) {
143
+ const geojson = data.__geojson;
144
+ return coordsLength(geojson.coordinates);
145
+ }
146
+ };
147
+ }
148
+ if (subtype === GisSubtype.MultiLineString) {
149
+ newFields.length = {
150
+ type: GraphQLFloat,
151
+ description: 'Approximate total length in meters (geodesic calculation from coordinates).',
152
+ resolve(data) {
153
+ const geojson = data.__geojson;
154
+ let total = 0;
155
+ for (const lineCoords of geojson.coordinates) {
156
+ total += coordsLength(lineCoords);
157
+ }
158
+ return total;
159
+ }
160
+ };
161
+ }
162
+ if (Object.keys(newFields).length === 0) {
163
+ return fields;
164
+ }
165
+ return build.extend(fields, newFields, 'PostgisMeasurementFieldsPlugin adding measurement fields');
166
+ }
167
+ }
168
+ }
169
+ };
@@ -0,0 +1,22 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ import type { GraphileConfig } from 'graphile-config';
4
+ import '../types';
5
+ /**
6
+ * PostgisTransformationFieldsPlugin
7
+ *
8
+ * Adds transformation fields to PostGIS geometry/geography object types:
9
+ *
10
+ * - `centroid`: The geometric centroid of the geometry (client-side calculation)
11
+ * - `bbox`: The bounding box of the geometry as [minX, minY, maxX, maxY]
12
+ * - `numPoints`: Number of coordinate points in the geometry
13
+ *
14
+ * These are client-side transformations computed from GeoJSON coordinates.
15
+ * For server-side PostGIS transformations (ST_Transform, ST_Buffer, ST_Simplify,
16
+ * ST_MakeValid), use SQL computed columns or custom mutations.
17
+ *
18
+ * Note: ST_Transform, ST_Buffer, ST_Simplify, and ST_MakeValid take parameters
19
+ * (target SRID, buffer distance, simplification tolerance) that make them better
20
+ * suited as custom SQL functions or mutation fields rather than static object fields.
21
+ */
22
+ export declare const PostgisTransformationFieldsPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,117 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ // Import types.ts for Build augmentation side effects
4
+ import '../types';
5
+ /**
6
+ * PostgisTransformationFieldsPlugin
7
+ *
8
+ * Adds transformation fields to PostGIS geometry/geography object types:
9
+ *
10
+ * - `centroid`: The geometric centroid of the geometry (client-side calculation)
11
+ * - `bbox`: The bounding box of the geometry as [minX, minY, maxX, maxY]
12
+ * - `numPoints`: Number of coordinate points in the geometry
13
+ *
14
+ * These are client-side transformations computed from GeoJSON coordinates.
15
+ * For server-side PostGIS transformations (ST_Transform, ST_Buffer, ST_Simplify,
16
+ * ST_MakeValid), use SQL computed columns or custom mutations.
17
+ *
18
+ * Note: ST_Transform, ST_Buffer, ST_Simplify, and ST_MakeValid take parameters
19
+ * (target SRID, buffer distance, simplification tolerance) that make them better
20
+ * suited as custom SQL functions or mutation fields rather than static object fields.
21
+ */
22
+ export const PostgisTransformationFieldsPlugin = {
23
+ name: 'PostgisTransformationFieldsPlugin',
24
+ version: '1.0.0',
25
+ description: 'Adds transformation fields (centroid, bbox, numPoints) to PostGIS geometry types',
26
+ after: ['PostgisRegisterTypesPlugin', 'PostgisGeometryFieldsPlugin'],
27
+ schema: {
28
+ hooks: {
29
+ GraphQLObjectType_fields(fields, build, context) {
30
+ const { isPgGISType, pgGISCodecName, pgGISTypeDetails } = context.scope;
31
+ if (!isPgGISType || !pgGISCodecName || !pgGISTypeDetails) {
32
+ return fields;
33
+ }
34
+ const { graphql: { GraphQLFloat, GraphQLInt, GraphQLList, GraphQLNonNull }, } = build;
35
+ const newFields = {};
36
+ // bbox: available for all geometry types
37
+ newFields.bbox = {
38
+ type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
39
+ description: 'Bounding box as [minX, minY, maxX, maxY]. ' +
40
+ 'Computed from GeoJSON coordinates.',
41
+ resolve(data) {
42
+ const coords = extractAllCoordinates(data.__geojson);
43
+ if (coords.length === 0)
44
+ return null;
45
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
46
+ for (const c of coords) {
47
+ if (c[0] < minX)
48
+ minX = c[0];
49
+ if (c[1] < minY)
50
+ minY = c[1];
51
+ if (c[0] > maxX)
52
+ maxX = c[0];
53
+ if (c[1] > maxY)
54
+ maxY = c[1];
55
+ }
56
+ return [minX, minY, maxX, maxY];
57
+ }
58
+ };
59
+ // centroid: available for all geometry types
60
+ newFields.centroid = {
61
+ type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
62
+ description: 'Centroid as [x, y] (arithmetic mean of all coordinate points). ' +
63
+ 'This is a simple centroid, not a weighted geographic centroid.',
64
+ resolve(data) {
65
+ const coords = extractAllCoordinates(data.__geojson);
66
+ if (coords.length === 0)
67
+ return null;
68
+ let sumX = 0, sumY = 0;
69
+ for (const c of coords) {
70
+ sumX += c[0];
71
+ sumY += c[1];
72
+ }
73
+ return [sumX / coords.length, sumY / coords.length];
74
+ }
75
+ };
76
+ // numPoints: count of coordinate points
77
+ newFields.numPoints = {
78
+ type: new GraphQLNonNull(GraphQLInt),
79
+ description: 'Total number of coordinate points in the geometry.',
80
+ resolve(data) {
81
+ return extractAllCoordinates(data.__geojson).length;
82
+ }
83
+ };
84
+ return build.extend(fields, newFields, 'PostgisTransformationFieldsPlugin adding transformation fields');
85
+ }
86
+ }
87
+ }
88
+ };
89
+ /**
90
+ * Recursively extracts all [x, y, ...] coordinate tuples from a GeoJSON geometry.
91
+ */
92
+ function extractAllCoordinates(geojson) {
93
+ const geo = geojson;
94
+ const type = geo.type;
95
+ switch (type) {
96
+ case 'Point':
97
+ return [geo.coordinates];
98
+ case 'MultiPoint':
99
+ case 'LineString':
100
+ return geo.coordinates;
101
+ case 'MultiLineString':
102
+ case 'Polygon':
103
+ return geo.coordinates.flat();
104
+ case 'MultiPolygon':
105
+ return geo.coordinates.flat(2);
106
+ case 'GeometryCollection': {
107
+ const geometries = geo.geometries;
108
+ const all = [];
109
+ for (const g of geometries) {
110
+ all.push(...extractAllCoordinates(g));
111
+ }
112
+ return all;
113
+ }
114
+ default:
115
+ return [];
116
+ }
117
+ }
@@ -0,0 +1,32 @@
1
+ import 'graphile-build';
2
+ import 'graphile-connection-filter';
3
+ import type { ConnectionFilterOperatorFactory } from 'graphile-connection-filter';
4
+ import '../types';
5
+ /**
6
+ * Creates the PostGIS ST_DWithin (withinDistance) operator factory.
7
+ *
8
+ * ST_DWithin is special among PostGIS spatial operators because it takes
9
+ * THREE arguments: (geom1, geom2, distance) instead of the standard two.
10
+ * This requires a compound GraphQL input type (geometry + distance) rather
11
+ * than just a geometry value.
12
+ *
13
+ * For geography columns, distance is in meters.
14
+ * For geometry columns, distance is in the SRID's coordinate units.
15
+ *
16
+ * @example
17
+ * ```graphql
18
+ * query {
19
+ * restaurants(where: {
20
+ * location: {
21
+ * withinDistance: {
22
+ * point: { type: "Point", coordinates: [-73.99, 40.73] }
23
+ * distance: 5000
24
+ * }
25
+ * }
26
+ * }) {
27
+ * nodes { name }
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ export declare function createWithinDistanceOperatorFactory(): ConnectionFilterOperatorFactory;
@@ -0,0 +1,122 @@
1
+ import 'graphile-build';
2
+ import 'graphile-connection-filter';
3
+ import sql from 'pg-sql2';
4
+ import { CONCRETE_SUBTYPES } from '../constants';
5
+ // Import types.ts for Build augmentation side effects
6
+ import '../types';
7
+ /**
8
+ * Creates the PostGIS ST_DWithin (withinDistance) operator factory.
9
+ *
10
+ * ST_DWithin is special among PostGIS spatial operators because it takes
11
+ * THREE arguments: (geom1, geom2, distance) instead of the standard two.
12
+ * This requires a compound GraphQL input type (geometry + distance) rather
13
+ * than just a geometry value.
14
+ *
15
+ * For geography columns, distance is in meters.
16
+ * For geometry columns, distance is in the SRID's coordinate units.
17
+ *
18
+ * @example
19
+ * ```graphql
20
+ * query {
21
+ * restaurants(where: {
22
+ * location: {
23
+ * withinDistance: {
24
+ * point: { type: "Point", coordinates: [-73.99, 40.73] }
25
+ * distance: 5000
26
+ * }
27
+ * }
28
+ * }) {
29
+ * nodes { name }
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ export function createWithinDistanceOperatorFactory() {
35
+ return (build) => {
36
+ const postgisInfo = build.pgGISExtensionInfo;
37
+ if (!postgisInfo) {
38
+ return [];
39
+ }
40
+ const { inflection } = build;
41
+ const { schemaName, geometryCodec, geographyCodec } = postgisInfo;
42
+ // Collect all GQL type names for geometry and geography (same as main factory)
43
+ const gqlTypeNamesByBase = {
44
+ geometry: [],
45
+ geography: []
46
+ };
47
+ const codecPairs = [['geometry', geometryCodec]];
48
+ if (geographyCodec) {
49
+ codecPairs.push(['geography', geographyCodec]);
50
+ }
51
+ for (const [baseKey, codec] of codecPairs) {
52
+ const typeName = codec.name;
53
+ gqlTypeNamesByBase[baseKey].push(inflection.gisInterfaceName(typeName));
54
+ for (const subtype of CONCRETE_SUBTYPES) {
55
+ for (const hasZ of [false, true]) {
56
+ for (const hasM of [false, true]) {
57
+ gqlTypeNamesByBase[baseKey].push(inflection.gisType(typeName, subtype, hasZ, hasM, 0));
58
+ }
59
+ }
60
+ }
61
+ }
62
+ // Lazily create the WithinDistanceInput GraphQL type (shared across all registrations)
63
+ let withinDistanceInputType = null;
64
+ const getWithinDistanceInputType = () => {
65
+ if (!withinDistanceInputType) {
66
+ const { GraphQLInputObjectType, GraphQLNonNull, GraphQLFloat } = build.graphql;
67
+ const GeoJSON = build.getTypeByName('GeoJSON');
68
+ if (!GeoJSON) {
69
+ throw new Error('PostGIS: GeoJSON type not found; ensure PostgisRegisterTypesPlugin runs before connection filter factory processing.');
70
+ }
71
+ withinDistanceInputType = new GraphQLInputObjectType({
72
+ name: 'WithinDistanceInput',
73
+ description: 'Input for distance-based spatial filtering via ST_DWithin. ' +
74
+ 'Distance is in meters for geography columns, or SRID coordinate units for geometry columns.',
75
+ fields: {
76
+ point: {
77
+ type: new GraphQLNonNull(GeoJSON),
78
+ description: 'Reference geometry to measure distance from.'
79
+ },
80
+ distance: {
81
+ type: new GraphQLNonNull(GraphQLFloat),
82
+ description: 'Maximum distance threshold.'
83
+ }
84
+ }
85
+ });
86
+ }
87
+ return withinDistanceInputType;
88
+ };
89
+ const sqlDWithinFn = sql.identifier(schemaName, 'st_dwithin');
90
+ const sqlGeomFromGeoJSON = sql.identifier(schemaName, 'st_geomfromgeojson');
91
+ // Build registrations for every geometry/geography type name
92
+ const registrations = [];
93
+ for (const [baseType, typeNames] of Object.entries(gqlTypeNamesByBase)) {
94
+ if (typeNames.length === 0)
95
+ continue;
96
+ for (const typeName of typeNames) {
97
+ const geographyCast = baseType === 'geography'
98
+ ? sql.fragment `::${sql.identifier(schemaName, 'geography')}`
99
+ : sql.fragment ``;
100
+ registrations.push({
101
+ typeNames: typeName,
102
+ operatorName: 'withinDistance',
103
+ spec: {
104
+ description: 'Is within the specified distance of the given geometry (ST_DWithin). ' +
105
+ 'Distance is in meters for geography, SRID units for geometry.',
106
+ resolveType: () => getWithinDistanceInputType(),
107
+ // We override SQL value generation since the standard pipeline
108
+ // expects a single geometry value, but we have a compound input.
109
+ resolveSqlValue: () => sql.null,
110
+ resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
111
+ const { point, distance } = input;
112
+ const geoJsonStr = sql.value(JSON.stringify(point));
113
+ const geomSql = sql.fragment `${sqlGeomFromGeoJSON}(${geoJsonStr}::text)${geographyCast}`;
114
+ return sql.fragment `${sqlDWithinFn}(${sqlIdentifier}, ${geomSql}, ${sql.value(distance)})`;
115
+ }
116
+ },
117
+ });
118
+ }
119
+ }
120
+ return registrations;
121
+ };
122
+ }
package/esm/preset.d.ts CHANGED
@@ -10,7 +10,10 @@ import type { GraphileConfig } from 'graphile-config';
10
10
  * - PostGIS extension auto-detection
11
11
  * - PostGIS inflection (type names for subtypes, Z/M variants)
12
12
  * - Geometry field plugins (coordinates, GeoJSON output)
13
- * - Connection filter operators (26 spatial operators via declarative factory API)
13
+ * - Measurement fields (area, length, perimeter on geometry types)
14
+ * - Transformation fields (centroid, bbox, numPoints on geometry types)
15
+ * - Aggregate function definitions (ST_Extent, ST_Union, ST_Collect, ST_ConvexHull)
16
+ * - Connection filter operators (26 spatial operators + withinDistance via declarative factory API)
14
17
  *
15
18
  * @example
16
19
  * ```typescript
package/esm/preset.js CHANGED
@@ -3,7 +3,11 @@ import { PostgisInflectionPlugin } from './plugins/inflection';
3
3
  import { PostgisExtensionDetectionPlugin } from './plugins/detect-extension';
4
4
  import { PostgisRegisterTypesPlugin } from './plugins/register-types';
5
5
  import { PostgisGeometryFieldsPlugin } from './plugins/geometry-fields';
6
+ import { PostgisMeasurementFieldsPlugin } from './plugins/measurement-fields';
7
+ import { PostgisTransformationFieldsPlugin } from './plugins/transformation-functions';
8
+ import { PostgisAggregatePlugin } from './plugins/aggregate-functions';
6
9
  import { createPostgisOperatorFactory } from './plugins/connection-filter-operators';
10
+ import { createWithinDistanceOperatorFactory } from './plugins/within-distance-operator';
7
11
  /**
8
12
  * GraphilePostgisPreset
9
13
  *
@@ -15,7 +19,10 @@ import { createPostgisOperatorFactory } from './plugins/connection-filter-operat
15
19
  * - PostGIS extension auto-detection
16
20
  * - PostGIS inflection (type names for subtypes, Z/M variants)
17
21
  * - Geometry field plugins (coordinates, GeoJSON output)
18
- * - Connection filter operators (26 spatial operators via declarative factory API)
22
+ * - Measurement fields (area, length, perimeter on geometry types)
23
+ * - Transformation fields (centroid, bbox, numPoints on geometry types)
24
+ * - Aggregate function definitions (ST_Extent, ST_Union, ST_Collect, ST_ConvexHull)
25
+ * - Connection filter operators (26 spatial operators + withinDistance via declarative factory API)
19
26
  *
20
27
  * @example
21
28
  * ```typescript
@@ -32,11 +39,16 @@ export const GraphilePostgisPreset = {
32
39
  PostgisInflectionPlugin,
33
40
  PostgisExtensionDetectionPlugin,
34
41
  PostgisRegisterTypesPlugin,
35
- PostgisGeometryFieldsPlugin
42
+ PostgisGeometryFieldsPlugin,
43
+ PostgisMeasurementFieldsPlugin,
44
+ PostgisTransformationFieldsPlugin,
45
+ PostgisAggregatePlugin
36
46
  ],
37
47
  schema: {
48
+ // connectionFilterOperatorFactories is augmented by graphile-connection-filter
38
49
  connectionFilterOperatorFactories: [
39
50
  createPostgisOperatorFactory(),
51
+ createWithinDistanceOperatorFactory(),
40
52
  ],
41
53
  },
42
54
  };