diff --git a/graphile/graphile-postgis/__tests__/codec.test.ts b/graphile/graphile-postgis/__tests__/codec.test.ts index dca3d5328..a802975b2 100644 --- a/graphile/graphile-postgis/__tests__/codec.test.ts +++ b/graphile/graphile-postgis/__tests__/codec.test.ts @@ -1,6 +1,8 @@ import type { PgCodec } from '@dataplan/pg'; import { PostgisCodecPlugin } from '../src/plugins/codec'; import type { GisFieldValue } from '../src/types'; +import { GisSubtype } from '../src/constants'; +import { getGISTypeModifier } from '../src/utils'; // Test event shape matching the GatherHooks.pgCodecs_findPgCodec event interface MockEvent { @@ -242,4 +244,83 @@ describe('PostgisCodecPlugin', () => { }); }); }); + + describe('pgCodecs_attribute hook', () => { + const attributeHook = (PostgisCodecPlugin as { gather: { hooks: { pgCodecs_attribute: Function } } }) + .gather.hooks.pgCodecs_attribute; + + async function runAttributeHook({ + codecName, + typmod, + withExtensions = true, + }: { + codecName: string; + typmod: number | null; + withExtensions?: boolean; + }) { + const attribute: Record = { codec: { name: codecName } }; + if (withExtensions) { + attribute.extensions = {}; + } + const event = { pgAttribute: { atttypmod: typmod }, attribute }; + + await attributeHook({}, event); + return attribute; + } + + it.each([ + ['geometry', GisSubtype.Polygon, 'Polygon'], + ['geometry', GisSubtype.Point, 'Point'], + ['geography', GisSubtype.MultiPolygon, 'MultiPolygon'], + ])('stores geometrySubtype for %s subtype %s', async (codecName, subtype, expected) => { + const attribute = await runAttributeHook({ + codecName, + typmod: getGISTypeModifier(subtype, false, false, 4326), + }); + + expect(attribute.extensions.geometrySubtype).toBe(expected); + }); + + it('should skip unconstrained geometry (atttypmod = -1)', async () => { + const attribute = await runAttributeHook({ + codecName: 'geometry', + typmod: -1, + }); + expect(attribute.extensions.geometrySubtype).toBeUndefined(); + }); + + it('should skip when atttypmod is null', async () => { + const attribute = await runAttributeHook({ + codecName: 'geometry', + typmod: null, + }); + expect(attribute.extensions.geometrySubtype).toBeUndefined(); + }); + + it('should not store subtype for base Geometry (subtype=0)', async () => { + const attribute = await runAttributeHook({ + codecName: 'geometry', + typmod: getGISTypeModifier(GisSubtype.Geometry, false, false, 4326), + }); + expect(attribute.extensions.geometrySubtype).toBeUndefined(); + }); + + it('should skip non-geometry codec types', async () => { + const attribute = await runAttributeHook({ + codecName: 'text', + typmod: getGISTypeModifier(GisSubtype.Point, false, false, 4326), + }); + expect(attribute.extensions.geometrySubtype).toBeUndefined(); + }); + + it('should create extensions object if not present', async () => { + const attribute = await runAttributeHook({ + codecName: 'geometry', + typmod: getGISTypeModifier(GisSubtype.LineString, false, false, 4326), + withExtensions: false, + }); + expect(attribute.extensions).toBeDefined(); + expect(attribute.extensions.geometrySubtype).toBe('LineString'); + }); + }); }); diff --git a/graphile/graphile-postgis/src/plugins/codec.ts b/graphile/graphile-postgis/src/plugins/codec.ts index 89803b5af..4049f2894 100644 --- a/graphile/graphile-postgis/src/plugins/codec.ts +++ b/graphile/graphile-postgis/src/plugins/codec.ts @@ -3,7 +3,9 @@ import type { PgCodec } from '@dataplan/pg'; import type { GraphileConfig } from 'graphile-config'; import type { SQL } from 'pg-sql2'; import sql from 'pg-sql2'; +import { GIS_SUBTYPE_NAME } from '../constants'; import type { GisFieldValue } from '../types'; +import { getGISTypeDetails } from '../utils'; /** * Map from PostGIS uppercase geometry type names (from geometrytype()) to @@ -52,6 +54,12 @@ function normalizeGisType(raw: string): string { return GIS_TYPE_NORMALIZE[raw] ?? raw; } +function getGeometrySubtypeName(typmod: number): string | null { + const { subtype } = getGISTypeDetails(typmod); + const subtypeName = GIS_SUBTYPE_NAME[subtype]; + return subtypeName && subtypeName !== 'Geometry' ? subtypeName : null; +} + /** * Scalar PgCodec for PostGIS geometry/geography types. * @@ -226,6 +234,41 @@ export const PostgisCodecPlugin: GraphileConfig.Plugin = { ); return; } + }, + + /** + * Annotate geometry/geography attributes with their subtype (Point, + * Polygon, LineString, etc.) decoded from the PostgreSQL type modifier. + * + * The atttypmod encodes subtype + SRID + Z/M flags. We decode it with + * getGISTypeDetails() and store the human-readable subtype name on + * attribute.extensions.geometrySubtype so the _meta plugin can expose it. + */ + async pgCodecs_attribute(_info, event) { + const { pgAttribute, attribute } = event; + const codecName = attribute.codec?.name; + if (codecName !== 'geometry' && codecName !== 'geography') { + return; + } + + const typmod = pgAttribute.atttypmod; + // atttypmod of -1 or null means no modifier (unconstrained geometry) + if (typmod == null || typmod === -1) { + return; + } + + try { + const subtypeName = getGeometrySubtypeName(typmod); + if (subtypeName) { + if (!attribute.extensions) { + attribute.extensions = {}; + } + (attribute.extensions as Record).geometrySubtype = subtypeName; + } + } catch { + // If the modifier can't be decoded, silently skip — the column + // will still work, just without subtype info in _meta. + } } } } diff --git a/graphile/graphile-settings/__tests__/__snapshots__/meta-schema.test.ts.snap b/graphile/graphile-settings/__tests__/__snapshots__/meta-schema.test.ts.snap index 5e66f3792..9b1320a34 100644 --- a/graphile/graphile-settings/__tests__/__snapshots__/meta-schema.test.ts.snap +++ b/graphile/graphile-settings/__tests__/__snapshots__/meta-schema.test.ts.snap @@ -21,6 +21,7 @@ exports[`MetaSchemaPlugin _meta query contract contains required selection paths "fields.type.gqlType", "fields.type.isArray", "fields.type.pgType", + "fields.type.subtype", "foreignKeyConstraints", "foreignKeyConstraints.fields", "foreignKeyConstraints.fields.name", @@ -118,6 +119,7 @@ exports[`MetaSchemaPlugin _meta query contract has stable printed GraphQL text 1 pgType gqlType isArray + subtype } } indexes { @@ -260,6 +262,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -278,6 +281,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -304,6 +308,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -322,6 +327,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -349,6 +355,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -370,6 +377,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, { @@ -385,6 +393,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "text", + "subtype": null, }, }, { @@ -400,6 +409,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, { @@ -415,6 +425,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -434,6 +445,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -452,6 +464,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -478,6 +491,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -496,6 +510,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -527,6 +542,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -566,6 +582,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -598,6 +615,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -623,6 +641,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -658,6 +677,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -676,6 +696,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -703,6 +724,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -724,6 +746,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, { @@ -739,6 +762,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, { @@ -754,6 +778,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": false, "pgType": "jsonb", + "subtype": null, }, }, { @@ -769,6 +794,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "text", + "subtype": null, }, }, ], @@ -788,6 +814,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -806,6 +833,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -837,6 +865,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -876,6 +905,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -908,6 +938,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -935,6 +966,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -960,6 +992,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -987,6 +1020,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1012,6 +1046,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1040,6 +1075,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1058,6 +1094,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1083,6 +1120,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1101,6 +1139,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1119,6 +1158,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1144,6 +1184,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1164,6 +1205,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1181,6 +1223,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1212,6 +1255,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1230,6 +1274,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1256,6 +1301,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1274,6 +1320,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1301,6 +1348,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, { @@ -1316,6 +1364,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1337,6 +1386,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, { @@ -1352,6 +1402,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1371,6 +1422,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1389,6 +1441,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1415,6 +1468,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1433,6 +1487,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1465,6 +1520,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, { @@ -1480,6 +1536,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1519,6 +1576,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, { @@ -1534,6 +1592,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1566,6 +1625,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1591,6 +1651,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1626,6 +1687,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1647,6 +1709,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "text", + "subtype": null, }, }, ], @@ -1668,6 +1731,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, { @@ -1683,6 +1747,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "text", + "subtype": null, }, }, ], @@ -1706,6 +1771,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1731,6 +1797,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "text", + "subtype": null, }, }, ], @@ -1770,6 +1837,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1803,6 +1871,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1830,6 +1899,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1858,6 +1928,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1876,6 +1947,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1901,6 +1973,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1919,6 +1992,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1937,6 +2011,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1962,6 +2037,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1982,6 +2058,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -1999,6 +2076,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -2026,6 +2104,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "text", + "subtype": null, }, }, ], @@ -2051,6 +2130,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -2072,6 +2152,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "email", + "subtype": null, }, }, ], @@ -2093,6 +2174,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": false, "pgType": "text", + "subtype": null, }, }, { @@ -2108,6 +2190,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "email", + "subtype": null, }, }, { @@ -2123,6 +2206,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -2146,6 +2230,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "email", + "subtype": null, }, }, ], @@ -2171,6 +2256,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -2210,6 +2296,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -2243,6 +2330,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -2268,6 +2356,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -2295,6 +2384,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -2320,6 +2410,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "uuid", + "subtype": null, }, }, ], @@ -2349,6 +2440,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult "isArray": false, "isNotNull": true, "pgType": "email", + "subtype": null, }, }, ], diff --git a/graphile/graphile-settings/__tests__/meta-schema.test.ts b/graphile/graphile-settings/__tests__/meta-schema.test.ts index b9e6a53b0..b2bffc544 100644 --- a/graphile/graphile-settings/__tests__/meta-schema.test.ts +++ b/graphile/graphile-settings/__tests__/meta-schema.test.ts @@ -346,7 +346,7 @@ query MetaContract { name schemaName query { all one create update delete } - fields { name type { pgType gqlType isArray } } + fields { name type { pgType gqlType isArray subtype } } indexes { name isUnique isPrimary columns fields { name } } constraints { primaryKey { name } @@ -403,6 +403,7 @@ const REQUIRED_META_QUERY_PATHS = [ 'fields.type.pgType', 'fields.type.gqlType', 'fields.type.isArray', + 'fields.type.subtype', 'indexes.name', 'indexes.isUnique', 'indexes.isPrimary', @@ -804,6 +805,7 @@ describe('MetaSchemaPlugin', () => { isArray: false, isNotNull: true, hasDefault: true, + subtype: null, }, isNotNull: true, hasDefault: true, @@ -857,6 +859,28 @@ describe('MetaSchemaPlugin', () => { expect(result.type.gqlType).toBe('Int'); expect(result.type.isArray).toBe(true); }); + + it('reads geometrySubtype from attribute extensions', () => { + const attr = createMockAttribute('geometry', { + extensions: { geometrySubtype: 'Polygon' }, + }); + const result = _buildFieldMeta('zoneBoundary', attr); + expect(result.type.subtype).toBe('Polygon'); + expect(result.type.gqlType).toBe('GeoJSON'); + }); + + it('returns null subtype when no geometrySubtype extension', () => { + const attr = createMockAttribute('geometry'); + const result = _buildFieldMeta('location', attr); + expect(result.type.subtype).toBeNull(); + expect(result.type.gqlType).toBe('GeoJSON'); + }); + + it('returns null subtype for non-geometry types', () => { + const attr = createMockAttribute('text'); + const result = _buildFieldMeta('name', attr); + expect(result.type.subtype).toBeNull(); + }); }); // --------------------------------------------------------------------------- diff --git a/graphile/graphile-settings/src/plugins/meta-schema/graphql-meta-field.ts b/graphile/graphile-settings/src/plugins/meta-schema/graphql-meta-field.ts index e89e42936..e62d3f4b5 100644 --- a/graphile/graphile-settings/src/plugins/meta-schema/graphql-meta-field.ts +++ b/graphile/graphile-settings/src/plugins/meta-schema/graphql-meta-field.ts @@ -31,6 +31,7 @@ function createMetaSchemaType(): GraphQLObjectType { isArray: { type: nn(GraphQLBoolean) }, isNotNull: { type: GraphQLBoolean }, hasDefault: { type: GraphQLBoolean }, + subtype: { type: GraphQLString }, }), }); diff --git a/graphile/graphile-settings/src/plugins/meta-schema/type-mappings.ts b/graphile/graphile-settings/src/plugins/meta-schema/type-mappings.ts index 25f7e69fc..261f8e334 100644 --- a/graphile/graphile-settings/src/plugins/meta-schema/type-mappings.ts +++ b/graphile/graphile-settings/src/plugins/meta-schema/type-mappings.ts @@ -44,6 +44,13 @@ export function pgTypeToGqlType(pgTypeName: string): string { return PG_TO_GQL_TYPE[pgTypeName] || pgTypeName; } +function getGeometrySubtype( + attr: PgAttribute | null | undefined, +): string | null { + const subtype = attr?.extensions?.geometrySubtype; + return typeof subtype === 'string' ? subtype : null; +} + function resolveGqlTypeName( build: GqlTypeResolverBuild | undefined, codec: PgCodec | null | undefined, @@ -82,6 +89,7 @@ export function buildFieldMeta( const pgType = attr?.codec?.name || 'unknown'; const isNotNull = attr?.notNull || false; const hasDefault = attr?.hasDefault || false; + const subtype = getGeometrySubtype(attr); return { name, @@ -91,6 +99,7 @@ export function buildFieldMeta( isArray: !!attr?.codec?.arrayOfCodec, isNotNull, hasDefault, + subtype, }, isNotNull, hasDefault, diff --git a/graphile/graphile-settings/src/plugins/meta-schema/types.ts b/graphile/graphile-settings/src/plugins/meta-schema/types.ts index 7cb7b86b9..29973adce 100644 --- a/graphile/graphile-settings/src/plugins/meta-schema/types.ts +++ b/graphile/graphile-settings/src/plugins/meta-schema/types.ts @@ -28,6 +28,7 @@ export interface TypeMeta { isArray: boolean; isNotNull?: boolean; hasDefault?: boolean; + subtype?: string | null; } export interface IndexMeta { @@ -135,11 +136,16 @@ export interface PgCodec { }; } +export interface PgAttributeExtensions extends Record { + geometrySubtype?: string | null; +} + export interface PgAttribute { codec?: PgCodec | null; notNull?: boolean; hasDefault?: boolean; description?: string | null; + extensions?: PgAttributeExtensions | null; } export interface PgUnique { diff --git a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap index d69d5d919..18944d7bf 100644 --- a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap +++ b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap @@ -1455,6 +1455,7 @@ type MetaType { isArray: Boolean! isNotNull: Boolean hasDefault: Boolean + subtype: String } """Information about a database index""" diff --git a/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap b/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap index 6adcdfd34..300db1937 100644 --- a/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap +++ b/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap @@ -1637,6 +1637,18 @@ based pagination. May not be used with \`last\`.", "ofType": null, }, }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "subtype", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null, + }, + }, ], "inputFields": null, "interfaces": [],