Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@stonecrop/graphql-middleware",
"comment": "use correct amber preset inflection for doctypes",
"type": "patch"
}
],
"packageName": "@stonecrop/graphql-middleware"
}
7 changes: 4 additions & 3 deletions graphql_middleware/src/plugin/postgraphile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,12 +468,13 @@ function defaultReverseConnectionName(params: {
target: string
}): string {
const targetPlural = pluralize.plural(params.target.replace(/-/g, '_'))
const targetPascal = toPascalCase(targetPlural)
// Use camelCase for target (matches PostGraphile Amber convention: recipeTasksByRecipeId)
const targetCamel = snakeToCamel(targetPlural)
// Use backlink if provided, otherwise derive from parent table name
const fkSource = params.backlink || params.doctype
// FK column name: uppercase first char, preserve rest of camelCase
const fkPascal = fkSource.charAt(0).toUpperCase() + fkSource.slice(1)
return `${targetPascal}By${fkPascal}Id`
const fkPascal = fkSource.charAt(0).toUpperCase() + snakeToCamel(fkSource).slice(1)
return `${targetCamel}By${fkPascal}Id`
}

/**
Expand Down
8 changes: 7 additions & 1 deletion graphql_middleware/src/registry/doctypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,13 @@ export function loadDoctypesFromObject(doctypes: Record<string, unknown>, option
* @public
*/
export function getMeta(name: string): DoctypeMeta | undefined {
return doctypeRegistry.get(name)
const direct = doctypeRegistry.get(name)
if (direct) return direct
// Fallback: find by slug (links reference doctypes by slug, not name)
for (const doctype of doctypeRegistry.values()) {
if (doctype.slug === name) return doctype
}
return undefined
}

/**
Expand Down
30 changes: 15 additions & 15 deletions graphql_middleware/tests/queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ describe('defaultReverseConnectionName', () => {
backlink: 'recipe',
target: 'recipe-task',
})
expect(result).toBe('RecipeTasksByRecipeId')
expect(result).toBe('recipeTasksByRecipeId')
})

it('handles self-referential links', () => {
Expand All @@ -329,7 +329,7 @@ describe('defaultReverseConnectionName', () => {
backlink: 'supersededBy',
target: 'recipe',
})
expect(result).toBe('RecipesBySupersededById')
expect(result).toBe('recipesBySupersededById')
})

it('handles underscored table names', () => {
Expand All @@ -339,7 +339,7 @@ describe('defaultReverseConnectionName', () => {
backlink: 'bom',
target: 'bom-item',
})
expect(result).toBe('BomItemsByBomId')
expect(result).toBe('bomItemsByBomId')
})
})

Expand Down Expand Up @@ -369,7 +369,7 @@ describe('reverseConnectionName override', () => {
{ includeNested: true }
)
// Default uses PostGraphile Amber convention
expect(query).toContain('RecipeTasksByRecipeId')
expect(query).toContain('recipeTasksByRecipeId')
})
})

Expand All @@ -387,7 +387,7 @@ describe('buildRecordQuery with includeNested', () => {
getMeta,
{ includeNested: true }
)
expect(query).toContain('RecipeTasksByRecipeId')
expect(query).toContain('recipeTasksByRecipeId')
expect(query).toContain('nodes')
expect(query).toContain('description')
})
Expand Down Expand Up @@ -415,7 +415,7 @@ describe('buildRecordQuery with includeNested', () => {
{ includeNested: true }
)
// recipe → recipe-task → recipe (seen, skipped)
expect(query).toContain('RecipeTasksByRecipeId')
expect(query).toContain('recipeTasksByRecipeId')
// Should not include nested recipe sub-selection (circular)
expect((query.match(/recipeById/g) || []).length).toBe(1) // only the outermost
})
Expand All @@ -429,7 +429,7 @@ describe('buildRecordQuery with includeNested', () => {
getMeta,
{ includeNested: true, maxDepth: 1 }
)
expect(query).toContain('RecipeTasksByRecipeId')
expect(query).toContain('recipeTasksByRecipeId')
// RecipeTask's links should not be included (depth 1 limit)
expect(query).not.toContain('recipe {')
})
Expand All @@ -444,7 +444,7 @@ describe('buildRecordQuery with includeNested', () => {
{ includeNested: ['supersededBy'] }
)
expect(query).toContain('supersededBy {')
expect(query).not.toContain('RecipeTasksByRecipeId')
expect(query).not.toContain('recipeTasksByRecipeId')
})

it('skips lazy links', () => {
Expand All @@ -466,7 +466,7 @@ describe('buildRecordQuery with includeNested', () => {
(slug: string) => lazyRegistry.get(slug),
{ includeNested: true }
)
expect(query).not.toContain('RecipeTasksByRecipeId')
expect(query).not.toContain('recipeTasksByRecipeId')
expect(query).toContain('name')
expect(query).toContain('status')
})
Expand Down Expand Up @@ -502,7 +502,7 @@ describe('buildRecordQuery with includeNested', () => {
(slug: string) => defaultRegistry.get(slug),
{ includeNested: true }
)
expect(query).toContain('RecipeTasksByRecipeId(first: 50)')
expect(query).toContain('recipeTasksByRecipeId(first: 50)')
})

it('blockWorkflows true forces lazy link into query', () => {
Expand All @@ -524,7 +524,7 @@ describe('buildRecordQuery with includeNested', () => {
(slug: string) => lazyWithBlockRegistry.get(slug),
{ includeNested: true }
)
expect(query).toContain('RecipeTasksByRecipeId')
expect(query).toContain('recipeTasksByRecipeId')
})
})

Expand All @@ -537,7 +537,7 @@ describe('mergeNestedResults', () => {
const record = {
id: 'r1',
name: 'Test Recipe',
RecipeTasksByRecipeId: {
recipeTasksByRecipeId: {
nodes: [
{ id: 't1', name: 'Task 1' },
{ id: 't2', name: 'Task 2' },
Expand All @@ -551,7 +551,7 @@ describe('mergeNestedResults', () => {
{ id: 't1', name: 'Task 1' },
{ id: 't2', name: 'Task 2' },
])
expect(result.RecipeTasksByRecipeId).toBeUndefined()
expect(result.recipeTasksByRecipeId).toBeUndefined()
})

it('leaves atMostOne links in place', () => {
Expand All @@ -570,7 +570,7 @@ describe('mergeNestedResults', () => {
const record = {
id: 'r1',
name: 'Test Recipe',
RecipeTasksByRecipeId: { nodes: [] },
recipeTasksByRecipeId: { nodes: [] },
}

const result = mergeNestedResults({ record, meta: recipeMeta, getMeta })
Expand Down Expand Up @@ -623,7 +623,7 @@ describe('mergeNestedResults', () => {
const record = {
id: 'r1',
name: 'Test Recipe',
RecipeTasksByRecipeId: {
recipeTasksByRecipeId: {
nodes: [{ id: 't1', name: 'Task 1' }],
},
}
Expand Down
10 changes: 10 additions & 0 deletions graphql_middleware/tests/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,16 @@ describe('getMeta / getAllMeta / hasMeta / clearRegistry', () => {
expect(meta?.name).toBe('Task')
})

it('getMeta finds doctype by slug when name lookup fails', () => {
loadDoctypesFromObject({
RecipeTask: { name: 'RecipeTask', slug: 'recipe-task', fields: [] },
})
expect(getMeta('RecipeTask')).toBeDefined()
expect(getMeta('recipe-task')).toBeDefined()
expect(getMeta('recipe-task')?.name).toBe('RecipeTask')
expect(getMeta('unknown-slug')).toBeUndefined()
})

it('hasMeta returns false before loading', () => {
expect(hasMeta('Task')).toBe(false)
})
Expand Down
Loading