Skip to content

Commit 21081ea

Browse files
committed
refactor: tighten geometry subtype meta coverage
1 parent ddedfa9 commit 21081ea

6 files changed

Lines changed: 72 additions & 50 deletions

File tree

graphile/graphile-postgis/__tests__/codec.test.ts

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -249,73 +249,76 @@ describe('PostgisCodecPlugin', () => {
249249
const attributeHook = (PostgisCodecPlugin as { gather: { hooks: { pgCodecs_attribute: Function } } })
250250
.gather.hooks.pgCodecs_attribute;
251251

252-
it('should store geometrySubtype for Polygon geometry column', async () => {
253-
const typmod = getGISTypeModifier(GisSubtype.Polygon, false, false, 4326);
254-
const attribute: Record<string, any> = { codec: { name: 'geometry' }, extensions: {} };
252+
async function runAttributeHook({
253+
codecName,
254+
typmod,
255+
withExtensions = true,
256+
}: {
257+
codecName: string;
258+
typmod: number | null;
259+
withExtensions?: boolean;
260+
}) {
261+
const attribute: Record<string, any> = { codec: { name: codecName } };
262+
if (withExtensions) {
263+
attribute.extensions = {};
264+
}
255265
const event = { pgAttribute: { atttypmod: typmod }, attribute };
256266

257267
await attributeHook({}, event);
258-
expect(attribute.extensions.geometrySubtype).toBe('Polygon');
259-
});
260-
261-
it('should store geometrySubtype for Point geometry column', async () => {
262-
const typmod = getGISTypeModifier(GisSubtype.Point, false, false, 4326);
263-
const attribute: Record<string, any> = { codec: { name: 'geometry' }, extensions: {} };
264-
const event = { pgAttribute: { atttypmod: typmod }, attribute };
265-
266-
await attributeHook({}, event);
267-
expect(attribute.extensions.geometrySubtype).toBe('Point');
268-
});
269-
270-
it('should store geometrySubtype for MultiPolygon geography column', async () => {
271-
const typmod = getGISTypeModifier(GisSubtype.MultiPolygon, false, false, 4326);
272-
const attribute: Record<string, any> = { codec: { name: 'geography' }, extensions: {} };
273-
const event = { pgAttribute: { atttypmod: typmod }, attribute };
268+
return attribute;
269+
}
270+
271+
it.each([
272+
['geometry', GisSubtype.Polygon, 'Polygon'],
273+
['geometry', GisSubtype.Point, 'Point'],
274+
['geography', GisSubtype.MultiPolygon, 'MultiPolygon'],
275+
])('stores geometrySubtype for %s subtype %s', async (codecName, subtype, expected) => {
276+
const attribute = await runAttributeHook({
277+
codecName,
278+
typmod: getGISTypeModifier(subtype, false, false, 4326),
279+
});
274280

275-
await attributeHook({}, event);
276-
expect(attribute.extensions.geometrySubtype).toBe('MultiPolygon');
281+
expect(attribute.extensions.geometrySubtype).toBe(expected);
277282
});
278283

279284
it('should skip unconstrained geometry (atttypmod = -1)', async () => {
280-
const attribute: Record<string, any> = { codec: { name: 'geometry' }, extensions: {} };
281-
const event = { pgAttribute: { atttypmod: -1 }, attribute };
282-
283-
await attributeHook({}, event);
285+
const attribute = await runAttributeHook({
286+
codecName: 'geometry',
287+
typmod: -1,
288+
});
284289
expect(attribute.extensions.geometrySubtype).toBeUndefined();
285290
});
286291

287292
it('should skip when atttypmod is null', async () => {
288-
const attribute: Record<string, any> = { codec: { name: 'geometry' }, extensions: {} };
289-
const event = { pgAttribute: { atttypmod: null as number | null }, attribute };
290-
291-
await attributeHook({}, event);
293+
const attribute = await runAttributeHook({
294+
codecName: 'geometry',
295+
typmod: null,
296+
});
292297
expect(attribute.extensions.geometrySubtype).toBeUndefined();
293298
});
294299

295300
it('should not store subtype for base Geometry (subtype=0)', async () => {
296-
const typmod = getGISTypeModifier(GisSubtype.Geometry, false, false, 4326);
297-
const attribute: Record<string, any> = { codec: { name: 'geometry' }, extensions: {} };
298-
const event = { pgAttribute: { atttypmod: typmod }, attribute };
299-
300-
await attributeHook({}, event);
301+
const attribute = await runAttributeHook({
302+
codecName: 'geometry',
303+
typmod: getGISTypeModifier(GisSubtype.Geometry, false, false, 4326),
304+
});
301305
expect(attribute.extensions.geometrySubtype).toBeUndefined();
302306
});
303307

304308
it('should skip non-geometry codec types', async () => {
305-
const typmod = getGISTypeModifier(GisSubtype.Point, false, false, 4326);
306-
const attribute: Record<string, any> = { codec: { name: 'text' }, extensions: {} };
307-
const event = { pgAttribute: { atttypmod: typmod }, attribute };
308-
309-
await attributeHook({}, event);
309+
const attribute = await runAttributeHook({
310+
codecName: 'text',
311+
typmod: getGISTypeModifier(GisSubtype.Point, false, false, 4326),
312+
});
310313
expect(attribute.extensions.geometrySubtype).toBeUndefined();
311314
});
312315

313316
it('should create extensions object if not present', async () => {
314-
const typmod = getGISTypeModifier(GisSubtype.LineString, false, false, 4326);
315-
const attribute: Record<string, any> = { codec: { name: 'geometry' } };
316-
const event = { pgAttribute: { atttypmod: typmod }, attribute };
317-
318-
await attributeHook({}, event);
317+
const attribute = await runAttributeHook({
318+
codecName: 'geometry',
319+
typmod: getGISTypeModifier(GisSubtype.LineString, false, false, 4326),
320+
withExtensions: false,
321+
});
319322
expect(attribute.extensions).toBeDefined();
320323
expect(attribute.extensions.geometrySubtype).toBe('LineString');
321324
});

graphile/graphile-postgis/src/plugins/codec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ function normalizeGisType(raw: string): string {
5454
return GIS_TYPE_NORMALIZE[raw] ?? raw;
5555
}
5656

57+
function getGeometrySubtypeName(typmod: number): string | null {
58+
const { subtype } = getGISTypeDetails(typmod);
59+
const subtypeName = GIS_SUBTYPE_NAME[subtype];
60+
return subtypeName && subtypeName !== 'Geometry' ? subtypeName : null;
61+
}
62+
5763
/**
5864
* Scalar PgCodec for PostGIS geometry/geography types.
5965
*
@@ -252,9 +258,8 @@ export const PostgisCodecPlugin: GraphileConfig.Plugin = {
252258
}
253259

254260
try {
255-
const details = getGISTypeDetails(typmod);
256-
const subtypeName = GIS_SUBTYPE_NAME[details.subtype];
257-
if (subtypeName && subtypeName !== 'Geometry') {
261+
const subtypeName = getGeometrySubtypeName(typmod);
262+
if (subtypeName) {
258263
if (!attribute.extensions) {
259264
attribute.extensions = {};
260265
}

graphile/graphile-settings/__tests__/__snapshots__/meta-schema.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ exports[`MetaSchemaPlugin _meta query contract contains required selection paths
2121
"fields.type.gqlType",
2222
"fields.type.isArray",
2323
"fields.type.pgType",
24+
"fields.type.subtype",
2425
"foreignKeyConstraints",
2526
"foreignKeyConstraints.fields",
2627
"foreignKeyConstraints.fields.name",
@@ -118,6 +119,7 @@ exports[`MetaSchemaPlugin _meta query contract has stable printed GraphQL text 1
118119
pgType
119120
gqlType
120121
isArray
122+
subtype
121123
}
122124
}
123125
indexes {

graphile/graphile-settings/__tests__/meta-schema.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ query MetaContract {
346346
name
347347
schemaName
348348
query { all one create update delete }
349-
fields { name type { pgType gqlType isArray } }
349+
fields { name type { pgType gqlType isArray subtype } }
350350
indexes { name isUnique isPrimary columns fields { name } }
351351
constraints {
352352
primaryKey { name }
@@ -403,6 +403,7 @@ const REQUIRED_META_QUERY_PATHS = [
403403
'fields.type.pgType',
404404
'fields.type.gqlType',
405405
'fields.type.isArray',
406+
'fields.type.subtype',
406407
'indexes.name',
407408
'indexes.isUnique',
408409
'indexes.isPrimary',

graphile/graphile-settings/src/plugins/meta-schema/type-mappings.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ export function pgTypeToGqlType(pgTypeName: string): string {
4444
return PG_TO_GQL_TYPE[pgTypeName] || pgTypeName;
4545
}
4646

47+
function getGeometrySubtype(
48+
attr: PgAttribute | null | undefined,
49+
): string | null {
50+
const subtype = attr?.extensions?.geometrySubtype;
51+
return typeof subtype === 'string' ? subtype : null;
52+
}
53+
4754
function resolveGqlTypeName(
4855
build: GqlTypeResolverBuild | undefined,
4956
codec: PgCodec | null | undefined,
@@ -82,7 +89,7 @@ export function buildFieldMeta(
8289
const pgType = attr?.codec?.name || 'unknown';
8390
const isNotNull = attr?.notNull || false;
8491
const hasDefault = attr?.hasDefault || false;
85-
const subtype = (attr?.extensions as Record<string, unknown> | null | undefined)?.geometrySubtype as string | null ?? null;
92+
const subtype = getGeometrySubtype(attr);
8693

8794
return {
8895
name,

graphile/graphile-settings/src/plugins/meta-schema/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,16 @@ export interface PgCodec {
136136
};
137137
}
138138

139+
export interface PgAttributeExtensions extends Record<string, unknown> {
140+
geometrySubtype?: string | null;
141+
}
142+
139143
export interface PgAttribute {
140144
codec?: PgCodec | null;
141145
notNull?: boolean;
142146
hasDefault?: boolean;
143147
description?: string | null;
144-
extensions?: Record<string, unknown> | null;
148+
extensions?: PgAttributeExtensions | null;
145149
}
146150

147151
export interface PgUnique {

0 commit comments

Comments
 (0)