graphile-postgis 2.6.6 → 2.7.0
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 +4 -0
- package/esm/index.js +5 -1
- package/esm/plugins/aggregate-functions.d.ts +23 -0
- package/esm/plugins/aggregate-functions.js +68 -0
- package/esm/plugins/measurement-fields.d.ts +17 -0
- package/esm/plugins/measurement-fields.js +169 -0
- package/esm/plugins/transformation-functions.d.ts +22 -0
- package/esm/plugins/transformation-functions.js +117 -0
- package/esm/plugins/within-distance-operator.d.ts +32 -0
- package/esm/plugins/within-distance-operator.js +122 -0
- package/esm/preset.d.ts +4 -1
- package/esm/preset.js +14 -2
- package/index.d.ts +4 -0
- package/index.js +10 -2
- package/package.json +2 -2
- package/plugins/aggregate-functions.d.ts +23 -0
- package/plugins/aggregate-functions.js +71 -0
- package/plugins/measurement-fields.d.ts +17 -0
- package/plugins/measurement-fields.js +172 -0
- package/plugins/transformation-functions.d.ts +22 -0
- package/plugins/transformation-functions.js +120 -0
- package/plugins/within-distance-operator.d.ts +32 -0
- package/plugins/within-distance-operator.js +128 -0
- package/preset.d.ts +4 -1
- package/preset.js +14 -2
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
|
-
|
|
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
|
-
* -
|
|
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
|
-
* -
|
|
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
|
};
|