Skip to content

Commit 4c7e6c7

Browse files
committed
refactor: consolidate duplicated fuzzy name-matching into shared utility
Extract duplicated fuzzy table-name matching logic from field-selector.ts and select.ts into a shared name-matching.ts module. Both files now import fuzzyFindByName from the shared utility instead of duplicating the normalize + compare logic inline. Also adds NOTE comments to codegen/utils.ts indicating that lcFirst, ucFirst, toCamelCase, toPascalCase, and toScreamingSnake can be replaced with re-exports from inflekt once version 0.4.0 is published (see dev-utils PR #71).
1 parent 57c2d5b commit 4c7e6c7

4 files changed

Lines changed: 80 additions & 26 deletions

File tree

graphql/codegen/src/core/codegen/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { scalarToFilterType, scalarToTsType } from './scalars';
1313

1414
// ============================================================================
1515
// String manipulation
16+
// NOTE: lcFirst, ucFirst already exist in inflekt; toCamelCase, toPascalCase,
17+
// toScreamingSnake are available in inflekt >=0.4.0. Once that version is
18+
// published these can be replaced with re-exports from inflekt.
1619
// ============================================================================
1720

1821
/** Lowercase first character */

graphql/query/src/generators/field-selector.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
FieldSelectionPreset,
1010
SimpleFieldSelection,
1111
} from '../types/selection';
12+
import { fuzzyFindByName } from './name-matching';
1213

1314
const relationalFieldSetCache = new WeakMap<Table, Set<string>>();
1415

@@ -293,19 +294,14 @@ function getRelatedTableScalarFields(
293294
return {};
294295
}
295296

296-
// Find the related table in allTables.
297-
// PostGraphile v5 uses different inflections in different contexts:
298-
// - table.name: PascalCase tableType (e.g., "Shipment", "DriverVehicleAssignment")
299-
// - relation referencedBy.name: raw codec name (e.g., "shipments", "driverVehicleAssignments")
300-
// Try exact match first, then case-insensitive match with optional trailing 's' for plural.
301-
const nameLower = referencedTableName.toLowerCase().replace(/_/g, '');
302-
const nameBase = nameLower.endsWith('s') ? nameLower.slice(0, -1) : nameLower;
303-
const relatedTable =
304-
allTables.find((t) => t.name === referencedTableName) ??
305-
allTables.find((t) => {
306-
const tLower = t.name.toLowerCase().replace(/_/g, '');
307-
return tLower === nameLower || tLower === nameBase;
308-
});
297+
// Find the related table in allTables using shared fuzzy matching.
298+
// Handles PascalCase table names vs snake_case/camelCase/plural codec names.
299+
// TODO: replace with fuzzyFindByName from inflekt once 0.4.0 is published
300+
const relatedTable = fuzzyFindByName(
301+
allTables,
302+
referencedTableName,
303+
(t) => t.name,
304+
);
309305
if (!relatedTable) {
310306
// Related table not found in schema — return fallback { __typename: true }
311307
// so the query remains valid (nodes need at least one subfield).
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Shared name-matching utilities for resolving PostGraphile v5 naming mismatches.
3+
*
4+
* PostGraphile v5 uses different inflection conventions in different contexts:
5+
* - Table types are PascalCase (e.g., "Shipment", "DeliveryZone")
6+
* - Relation codec names are raw snake_case or camelCase (e.g., "shipments", "deliveryZones")
7+
*
8+
* These helpers provide a single, shared way to normalize and compare names
9+
* across those boundaries instead of duplicating fuzzy-match logic in every consumer.
10+
*
11+
* NOTE: These functions are also available in inflekt >=0.4.0
12+
* (fuzzyFindByName, normalizeName, normalizeNameSingular, namesMatch).
13+
* Once inflekt 0.4.0 is published and the dependency is bumped, the consumers
14+
* can import directly from inflekt and this file can be removed.
15+
*/
16+
17+
/**
18+
* Normalize a name for case-insensitive, delimiter-insensitive comparison.
19+
* Strips underscores and lowercases.
20+
*/
21+
function normalizeName(name: string): string {
22+
return name.toLowerCase().replace(/_/g, '');
23+
}
24+
25+
/**
26+
* Normalize a name to its singular base form for comparison.
27+
* Strips underscores, lowercases, and removes a trailing 's' when present.
28+
*/
29+
function normalizeNameSingular(name: string): string {
30+
const normalized = normalizeName(name);
31+
return normalized.endsWith('s') ? normalized.slice(0, -1) : normalized;
32+
}
33+
34+
/**
35+
* Find a matching item by name using exact match first, then fuzzy
36+
* case-insensitive / plural-insensitive fallback.
37+
*
38+
* This is the single shared implementation for resolving relation target names
39+
* to table definitions, replacing ad-hoc fuzzy matching scattered across consumers.
40+
*
41+
* @param items - Array of items to search through
42+
* @param targetName - The name to find (may be PascalCase, snake_case, plural, etc.)
43+
* @param getName - Accessor to extract the comparable name from each item
44+
* @returns The matched item, or undefined if no match found
45+
*/
46+
export function fuzzyFindByName<T>(
47+
items: T[],
48+
targetName: string,
49+
getName: (item: T) => string,
50+
): T | undefined {
51+
// 1. Exact match (fast path)
52+
const exact = items.find((item) => getName(item) === targetName);
53+
if (exact) return exact;
54+
55+
// 2. Fuzzy match: case-insensitive, strip underscores, optional trailing 's'
56+
const targetNormalized = normalizeName(targetName);
57+
const targetBase = normalizeNameSingular(targetName);
58+
59+
return items.find((item) => {
60+
const itemNormalized = normalizeName(getName(item));
61+
return itemNormalized === targetNormalized || itemNormalized === targetBase;
62+
});
63+
}

graphql/query/src/generators/select.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { QueryOptions } from '../types/query';
2525
import type { Table } from '../types/schema';
2626
import type { FieldSelection } from '../types/selection';
2727
import { convertToSelectionOptions, isRelationalField } from './field-selector';
28+
import { fuzzyFindByName } from './name-matching';
2829
import {
2930
normalizeInflectionValue,
3031
toCamelCasePlural,
@@ -787,19 +788,10 @@ function findRelatedTable(
787788
return null;
788789
}
789790

790-
// Find the related table in allTables
791-
const exactMatch = allTables.find((tbl) => tbl.name === referencedTableName);
792-
if (exactMatch) return exactMatch;
793-
794-
// Fuzzy match: case-insensitive, strip underscores, optional trailing 's'.
795-
// Needed because relation target names from _meta use snake_case codec names
796-
// (e.g. "routes", "delivery_zone") while allTables[].name is PascalCase (e.g. "Route", "DeliveryZone").
797-
const nameLower = referencedTableName.toLowerCase().replace(/_/g, '');
798-
const nameBase = nameLower.endsWith('s') ? nameLower.slice(0, -1) : nameLower;
799-
return allTables.find((tbl) => {
800-
const tLower = tbl.name.toLowerCase().replace(/_/g, '');
801-
return tLower === nameLower || tLower === nameBase;
802-
}) || null;
791+
// Find the related table using shared fuzzy matching.
792+
// Handles PascalCase table names vs snake_case/camelCase/plural codec names.
793+
// TODO: replace with fuzzyFindByName from inflekt once 0.4.0 is published
794+
return fuzzyFindByName(allTables, referencedTableName, (tbl) => tbl.name) ?? null;
803795
}
804796

805797
/**

0 commit comments

Comments
 (0)