Skip to content

Commit a66d3f4

Browse files
committed
fix(graphile-settings): resolve function/table resource name collisions
Add functionResourceName override to InflektPlugin to restore schema prefixes for functions while keeping clean names for tables. Filter ConflictDetectorPlugin by configured schemas to prevent false positives.
1 parent e8d5dbd commit a66d3f4

2 files changed

Lines changed: 100 additions & 81 deletions

File tree

graphile/graphile-settings/src/plugins/conflict-detector.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GraphileConfig } from 'graphile-config';
1+
import type { GraphileConfig } from "graphile-config";
22

33
/**
44
* Plugin that detects naming conflicts between tables in different schemas.
@@ -20,27 +20,40 @@ interface CodecInfo {
2020
}
2121

2222
export const ConflictDetectorPlugin: GraphileConfig.Plugin = {
23-
name: 'ConflictDetectorPlugin',
24-
version: '1.0.0',
23+
name: "ConflictDetectorPlugin",
24+
version: "1.0.0",
2525

2626
schema: {
2727
hooks: {
2828
build(build) {
2929
// Track codecs by their GraphQL name to detect conflicts
3030
const codecsByName = new Map<string, CodecInfo[]>();
3131

32+
// Get configured schemas from pgServices to only check relevant codecs
33+
const configuredSchemas = new Set<string>();
34+
const pgServices = (build as any).resolvedPreset?.pgServices ?? [];
35+
36+
for (const service of pgServices) {
37+
for (const schema of service.schemas ?? ["public"]) {
38+
configuredSchemas.add(schema);
39+
}
40+
}
41+
3242
// Iterate through all codecs to find tables
3343
for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
3444
// Skip non-table codecs (those without attributes or anonymous ones)
3545
if (!codec.attributes || codec.isAnonymous) continue;
3646

3747
// Get the schema name from the codec's extensions
38-
const pgExtensions = codec.extensions?.pg as
39-
| { schemaName?: string }
40-
| undefined;
41-
const schemaName = pgExtensions?.schemaName || 'unknown';
48+
const pgExtensions = codec.extensions?.pg as { schemaName?: string } | undefined;
49+
const schemaName = pgExtensions?.schemaName || "unknown";
4250
const tableName = codec.name;
4351

52+
// Skip codecs from schemas not in the configured list
53+
if (configuredSchemas.size > 0 && !configuredSchemas.has(schemaName)) {
54+
continue;
55+
}
56+
4457
// Get the GraphQL name that would be generated
4558
const graphqlName = build.inflection.tableType(codec);
4659

@@ -59,17 +72,15 @@ export const ConflictDetectorPlugin: GraphileConfig.Plugin = {
5972
// Check for conflicts and log warnings
6073
for (const [graphqlName, codecs] of codecsByName) {
6174
if (codecs.length > 1) {
62-
const locations = codecs
63-
.map((c) => `${c.schemaName}.${c.tableName}`)
64-
.join(', ');
75+
const locations = codecs.map((c) => `${c.schemaName}.${c.tableName}`).join(", ");
6576

6677
console.warn(
6778
`\nNAMING CONFLICT DETECTED: GraphQL type "${graphqlName}" would be generated from multiple tables:\n` +
6879
` Tables: ${locations}\n` +
6980
` Resolution options:\n` +
7081
` 1. Add @name smart tag to one table: COMMENT ON TABLE schema.table IS E'@name UniqueTypeName';\n` +
7182
` 2. Rename one of the tables in the database\n` +
72-
` 3. Exclude one table from the schema using @omit smart tag\n`
83+
` 3. Exclude one table from the schema using @omit smart tag\n`,
7384
);
7485
}
7586
}

graphile/graphile-settings/src/plugins/custom-inflector.ts

Lines changed: 78 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GraphileConfig } from 'graphile-config';
1+
import type { GraphileConfig } from "graphile-config";
22
import {
33
singularize,
44
pluralize,
@@ -7,7 +7,7 @@ import {
77
distinctPluralize,
88
fixCapitalisedPlural,
99
camelize,
10-
} from 'inflekt';
10+
} from "inflekt";
1111

1212
/**
1313
* Custom inflector plugin for Constructive using the inflekt library.
@@ -30,22 +30,22 @@ import {
3030
* Add your own mappings here as needed.
3131
*/
3232
const CUSTOM_OPPOSITES: Record<string, string> = {
33-
parent: 'child',
34-
child: 'parent',
35-
author: 'authored',
36-
editor: 'edited',
37-
reviewer: 'reviewed',
38-
owner: 'owned',
39-
creator: 'created',
40-
updater: 'updated',
33+
parent: "child",
34+
child: "parent",
35+
author: "authored",
36+
editor: "edited",
37+
reviewer: "reviewed",
38+
owner: "owned",
39+
creator: "created",
40+
updater: "updated",
4141
};
4242

4343
/**
4444
* Extract base name from attribute names like "author_id" -> "author"
4545
*/
4646
function getBaseName(attributeName: string): string | null {
4747
const matches = attributeName.match(
48-
/^(.+?)(_row_id|_id|_uuid|_fk|_pk|RowId|Id|Uuid|UUID|Fk|Pk)$/
48+
/^(.+?)(_row_id|_id|_uuid|_fk|_pk|RowId|Id|Uuid|UUID|Fk|Pk)$/,
4949
);
5050
if (matches) {
5151
return matches[1];
@@ -74,7 +74,7 @@ function getOppositeBaseName(baseName: string): string | null {
7474
function arraysMatch<T>(
7575
array1: readonly T[],
7676
array2: readonly T[],
77-
comparator: (v1: T, v2: T) => boolean = (v1, v2) => v1 === v2
77+
comparator: (v1: T, v2: T) => boolean = (v1, v2) => v1 === v2,
7878
): boolean {
7979
if (array1 === array2) return true;
8080
const l = array1.length;
@@ -86,8 +86,8 @@ function arraysMatch<T>(
8686
}
8787

8888
export const InflektPlugin: GraphileConfig.Plugin = {
89-
name: 'InflektPlugin',
90-
version: '1.0.0',
89+
name: "InflektPlugin",
90+
version: "1.0.0",
9191

9292
inflection: {
9393
replace: {
@@ -126,7 +126,40 @@ export const InflektPlugin: GraphileConfig.Plugin = {
126126
* same name in different schemas. Use @name smart tags to disambiguate.
127127
*/
128128
_schemaPrefix(_previous, _options, _details) {
129-
return '';
129+
return "";
130+
},
131+
132+
/**
133+
* Restore PostGraphile's default schema prefix logic for functions.
134+
*
135+
* Our _schemaPrefix override returns '' for all schemas to give clean
136+
* table names. However, this strips prefixes from functions too, causing
137+
* resource naming collisions when a function and table share the same
138+
* base name across schemas (e.g., actions_public.table_grant() collides
139+
* with metaschema_public.table_grant table).
140+
*
141+
* Fix: bypass our _schemaPrefix override for functions and use
142+
* PostGraphile's default prefix logic instead.
143+
*/
144+
functionResourceName(_previous, options: any, details: any) {
145+
const { serviceName, pgProc } = details;
146+
const { tags } = pgProc.getTagsAndDescription();
147+
148+
if (typeof tags.name === "string") {
149+
return tags.name;
150+
}
151+
152+
const pgNamespace = pgProc.getNamespace();
153+
154+
if (!pgNamespace) {
155+
return pgProc.proname;
156+
}
157+
158+
const pgService = (options.pgServices ?? []).find((db: any) => db.name === serviceName);
159+
const primarySchema = pgService?.schemas?.[0] ?? "public";
160+
const databasePrefix = serviceName === "main" ? "" : `${serviceName}_`;
161+
const schemaPrefix = pgNamespace.nspname === primarySchema ? "" : `${pgNamespace.nspname}_`;
162+
return `${databasePrefix}${schemaPrefix}${pgProc.proname}`;
130163
},
131164

132165
/**
@@ -167,7 +200,10 @@ export const InflektPlugin: GraphileConfig.Plugin = {
167200
_attributeName(
168201
_previous,
169202
_options,
170-
details: { attributeName: string; codec: { attributes: Record<string, { extensions?: { tags?: { name?: string } } }> } }
203+
details: {
204+
attributeName: string;
205+
codec: { attributes: Record<string, { extensions?: { tags?: { name?: string } } }> };
206+
},
171207
) {
172208
const attribute = details.codec.attributes[details.attributeName];
173209
const name = attribute?.extensions?.tags?.name || details.attributeName;
@@ -211,7 +247,7 @@ export const InflektPlugin: GraphileConfig.Plugin = {
211247
*/
212248
allRowsList(_previous, _options, resource) {
213249
const resourceName = this._singularizedResourceName(resource);
214-
return camelize(distinctPluralize(resourceName), true) + 'List';
250+
return camelize(distinctPluralize(resourceName), true) + "List";
215251
},
216252

217253
/**
@@ -220,7 +256,7 @@ export const InflektPlugin: GraphileConfig.Plugin = {
220256
singleRelation(previous, _options, details) {
221257
const { registry, codec, relationName } = details;
222258
const relation = registry.pgRelations[codec.name]?.[relationName];
223-
if (typeof relation.extensions?.tags?.fieldName === 'string') {
259+
if (typeof relation.extensions?.tags?.fieldName === "string") {
224260
return relation.extensions.tags.fieldName;
225261
}
226262

@@ -235,16 +271,10 @@ export const InflektPlugin: GraphileConfig.Plugin = {
235271

236272
// Fall back to the remote resource name
237273
const foreignPk = relation.remoteResource.uniques.find(
238-
(u: { isPrimary: boolean }) => u.isPrimary
274+
(u: { isPrimary: boolean }) => u.isPrimary,
239275
);
240-
if (
241-
foreignPk &&
242-
arraysMatch(foreignPk.attributes, relation.remoteAttributes)
243-
) {
244-
return camelize(
245-
this._singularizedCodecName(relation.remoteResource.codec),
246-
true
247-
);
276+
if (foreignPk && arraysMatch(foreignPk.attributes, relation.remoteAttributes)) {
277+
return camelize(this._singularizedCodecName(relation.remoteResource.codec), true);
248278
}
249279
return previous!(details);
250280
},
@@ -255,12 +285,10 @@ export const InflektPlugin: GraphileConfig.Plugin = {
255285
singleRelationBackwards(previous, _options, details) {
256286
const { registry, codec, relationName } = details;
257287
const relation = registry.pgRelations[codec.name]?.[relationName];
258-
if (
259-
typeof relation.extensions?.tags?.foreignSingleFieldName === 'string'
260-
) {
288+
if (typeof relation.extensions?.tags?.foreignSingleFieldName === "string") {
261289
return relation.extensions.tags.foreignSingleFieldName;
262290
}
263-
if (typeof relation.extensions?.tags?.foreignFieldName === 'string') {
291+
if (typeof relation.extensions?.tags?.foreignFieldName === "string") {
264292
return relation.extensions.tags.foreignFieldName;
265293
}
266294

@@ -273,14 +301,11 @@ export const InflektPlugin: GraphileConfig.Plugin = {
273301
if (oppositeBaseName) {
274302
return camelize(
275303
`${oppositeBaseName}_${this._singularizedCodecName(relation.remoteResource.codec)}`,
276-
true
304+
true,
277305
);
278306
}
279307
if (baseNameMatches(baseName, codec.name)) {
280-
return camelize(
281-
this._singularizedCodecName(relation.remoteResource.codec),
282-
true
283-
);
308+
return camelize(this._singularizedCodecName(relation.remoteResource.codec), true);
284309
}
285310
}
286311
}
@@ -295,7 +320,7 @@ export const InflektPlugin: GraphileConfig.Plugin = {
295320
const { registry, codec, relationName } = details;
296321
const relation = registry.pgRelations[codec.name]?.[relationName];
297322
const baseOverride = relation.extensions?.tags.foreignFieldName;
298-
if (typeof baseOverride === 'string') {
323+
if (typeof baseOverride === "string") {
299324
return baseOverride;
300325
}
301326

@@ -308,30 +333,24 @@ export const InflektPlugin: GraphileConfig.Plugin = {
308333
if (oppositeBaseName) {
309334
return camelize(
310335
`${oppositeBaseName}_${distinctPluralize(this._singularizedCodecName(relation.remoteResource.codec))}`,
311-
true
336+
true,
312337
);
313338
}
314339
if (baseNameMatches(baseName, codec.name)) {
315340
return camelize(
316-
distinctPluralize(
317-
this._singularizedCodecName(relation.remoteResource.codec)
318-
),
319-
true
341+
distinctPluralize(this._singularizedCodecName(relation.remoteResource.codec)),
342+
true,
320343
);
321344
}
322345
}
323346
}
324347

325348
// Fall back to pluralized remote resource name
326-
const pk = relation.remoteResource.uniques.find(
327-
(u: { isPrimary: boolean }) => u.isPrimary
328-
);
349+
const pk = relation.remoteResource.uniques.find((u: { isPrimary: boolean }) => u.isPrimary);
329350
if (pk && arraysMatch(pk.attributes, relation.remoteAttributes)) {
330351
return camelize(
331-
distinctPluralize(
332-
this._singularizedCodecName(relation.remoteResource.codec)
333-
),
334-
true
352+
distinctPluralize(this._singularizedCodecName(relation.remoteResource.codec)),
353+
true,
335354
);
336355
}
337356
return previous!(details);
@@ -349,19 +368,17 @@ export const InflektPlugin: GraphileConfig.Plugin = {
349368
* - There are multiple many-to-many relations to the same target table
350369
*/
351370
_manyToManyRelation(previous, _options, details) {
352-
const { leftTable, rightTable, junctionTable, rightRelationName } =
353-
details;
371+
const { leftTable, rightTable, junctionTable, rightRelationName } = details;
354372

355373
const junctionRightRelation = junctionTable.getRelation(rightRelationName);
356-
const baseOverride =
357-
junctionRightRelation.extensions?.tags?.manyToManyFieldName;
358-
if (typeof baseOverride === 'string') {
374+
const baseOverride = junctionRightRelation.extensions?.tags?.manyToManyFieldName;
375+
if (typeof baseOverride === "string") {
359376
return baseOverride;
360377
}
361378

362379
const simpleName = camelize(
363380
distinctPluralize(this._singularizedCodecName(rightTable.codec)),
364-
true
381+
true,
365382
);
366383

367384
const leftRelations = leftTable.getRelations();
@@ -374,10 +391,7 @@ export const InflektPlugin: GraphileConfig.Plugin = {
374391
hasDirectRelation = true;
375392
}
376393
}
377-
if (
378-
rel.isReferencee &&
379-
rel.remoteResource?.codec?.name !== rightTable.codec.name
380-
) {
394+
if (rel.isReferencee && rel.remoteResource?.codec?.name !== rightTable.codec.name) {
381395
const junctionRelations = rel.remoteResource?.getRelations?.() || {};
382396
for (const [_jRelName, jRel] of Object.entries(junctionRelations)) {
383397
if (
@@ -402,7 +416,7 @@ export const InflektPlugin: GraphileConfig.Plugin = {
402416
*/
403417
rowByUnique(previous, _options, details) {
404418
const { unique, resource } = details;
405-
if (typeof unique.extensions?.tags?.fieldName === 'string') {
419+
if (typeof unique.extensions?.tags?.fieldName === "string") {
406420
return unique.extensions?.tags?.fieldName;
407421
}
408422
if (unique.isPrimary) {
@@ -416,14 +430,11 @@ export const InflektPlugin: GraphileConfig.Plugin = {
416430
*/
417431
updateByKeysField(previous, _options, details) {
418432
const { resource, unique } = details;
419-
if (typeof unique.extensions?.tags.updateFieldName === 'string') {
433+
if (typeof unique.extensions?.tags.updateFieldName === "string") {
420434
return unique.extensions.tags.updateFieldName;
421435
}
422436
if (unique.isPrimary) {
423-
return camelize(
424-
`update_${this._singularizedCodecName(resource.codec)}`,
425-
true
426-
);
437+
return camelize(`update_${this._singularizedCodecName(resource.codec)}`, true);
427438
}
428439
return previous!(details);
429440
},
@@ -433,14 +444,11 @@ export const InflektPlugin: GraphileConfig.Plugin = {
433444
*/
434445
deleteByKeysField(previous, _options, details) {
435446
const { resource, unique } = details;
436-
if (typeof unique.extensions?.tags.deleteFieldName === 'string') {
447+
if (typeof unique.extensions?.tags.deleteFieldName === "string") {
437448
return unique.extensions.tags.deleteFieldName;
438449
}
439450
if (unique.isPrimary) {
440-
return camelize(
441-
`delete_${this._singularizedCodecName(resource.codec)}`,
442-
true
443-
);
451+
return camelize(`delete_${this._singularizedCodecName(resource.codec)}`, true);
444452
}
445453
return previous!(details);
446454
},

0 commit comments

Comments
 (0)