diff --git a/graphile/graphile-connection-filter/src/index.ts b/graphile/graphile-connection-filter/src/index.ts index 30c95ea35..44faf101f 100644 --- a/graphile/graphile-connection-filter/src/index.ts +++ b/graphile/graphile-connection-filter/src/index.ts @@ -49,6 +49,13 @@ * ``` */ +// Load the global type augmentations (inflection methods, build/scope +// properties) so that downstream satellite plugins which `import +// 'graphile-connection-filter'` pick up the `filterType`/`filterManyType`/ +// etc. type extensions without having to reach into the package's internal +// file layout. +import './augmentations'; + export { ConnectionFilterPreset } from './preset'; // Re-export all plugins for granular use diff --git a/graphile/graphile-postgis/__tests__/spatial-relations.test.ts b/graphile/graphile-postgis/__tests__/spatial-relations.test.ts index 63165312b..d9fe6b1e8 100644 --- a/graphile/graphile-postgis/__tests__/spatial-relations.test.ts +++ b/graphile/graphile-postgis/__tests__/spatial-relations.test.ts @@ -67,8 +67,16 @@ function buildMockRegistry( } function makeBuild(registry: any): any { + // Minimal inflection double — only `camelCase` is consulted by + // `collectSpatialRelations` (to normalize the parametric arg name). + const inflection = { + camelCase(str: string): string { + return str.replace(/[-_](.)/g, (_, c: string) => c.toUpperCase()); + }, + }; return { input: { pgRegistry: registry }, + inflection, }; } @@ -289,6 +297,27 @@ describe('collectSpatialRelations', () => { expect(rel.paramFieldName).toBe('distance'); }); + it('camelCases snake_case parametric arg names', () => { + // The @spatialRelation tag grammar accepts any [A-Za-z_][A-Za-z0-9_]* + // identifier for the parametric arg; the GraphQL field we expose for + // it must follow the same camelCase convention as every other field. + const registry = buildMockRegistry({ + clinics: { + pk: ['id'], + attributes: { + id: { base: 'int4' }, + location: { + base: 'geometry', + spatialRelation: + 'nearbyClinic clinics.location st_dwithin travel_distance', + }, + }, + }, + }); + const [rel] = collectSpatialRelations(makeBuild(registry)); + expect(rel.paramFieldName).toBe('travelDistance'); + }); + it('supports multiple tags on the same column (string[] form)', () => { const registry = buildMockRegistry({ counties: { diff --git a/graphile/graphile-postgis/src/plugins/spatial-relations.ts b/graphile/graphile-postgis/src/plugins/spatial-relations.ts index daf4984ea..ba0f5f3aa 100644 --- a/graphile/graphile-postgis/src/plugins/spatial-relations.ts +++ b/graphile/graphile-postgis/src/plugins/spatial-relations.ts @@ -322,6 +322,13 @@ export function collectSpatialRelations(build: any): SpatialRelationInfo[] { const pgRegistry = build.input?.pgRegistry; if (!pgRegistry) return []; + // Inflection is used to normalize user-supplied identifiers (the + // parametric arg name, e.g. `travel_distance` → `travelDistance`) into the + // GraphQL casing conventions. Fall back to identity if not available + // (e.g. when invoked from unit tests with a stub build). + const camelCase: (s: string) => string = + build.inflection?.camelCase?.bind(build.inflection) ?? ((s: string) => s); + const relations: SpatialRelationInfo[] = []; for (const resource of Object.values(pgRegistry.pgResources) as any[]) { @@ -400,7 +407,7 @@ export function collectSpatialRelations(build: any): SpatialRelationInfo[] { targetResource: target.resource, targetAttributeName: target.attributeName, operator: OPERATOR_REGISTRY[parsed.operator], - paramFieldName: parsed.paramName, + paramFieldName: parsed.paramName ? camelCase(parsed.paramName) : null, isSelfRelation, ownerPkAttributes, targetPkAttributes, @@ -431,7 +438,10 @@ export function collectSpatialRelations(build: any): SpatialRelationInfo[] { function spatialFilterTypeName(build: any, rel: SpatialRelationInfo): string { const { inflection } = build; const ownerTypeName = inflection.tableType(rel.ownerCodec); - const rel0 = rel.relationName.charAt(0).toUpperCase() + rel.relationName.slice(1); + // Normalize the user-supplied relation name (which may be snake_case, + // kebab-case, or mixed) into PascalCase so the type name is consistent + // with every other generated GraphQL type name. + const rel0 = inflection.upperCamelCase(rel.relationName); return `${ownerTypeName}Spatial${rel0}Filter`; } @@ -638,7 +648,10 @@ export const PostgisSpatialRelationsPlugin: GraphileConfig.Plugin = { const FilterType = build.getTypeByName(filterTypeName); if (!FilterType) continue; - const fieldName = rel.relationName; + // Normalize the user-supplied relation name (which may be + // snake_case, kebab-case, or mixed) into camelCase so the GraphQL + // field name matches the casing of every other generated field. + const fieldName = inflection.camelCase(rel.relationName); // Avoid clobbering fields an upstream plugin may have registered // (e.g. an FK-derived relation with the same name). if (fields[fieldName]) {