Skip to content

feat(cli): record the GraphQL kind of every named type - #17542

Open
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1787770434-graphql-type-categories
Open

feat(cli): record the GraphQL kind of every named type#17542
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1787770434-graphql-type-categories

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

Linear ticket: Refs

Phase 1 of GraphQL Types rendering: the converter now reports which GraphQL kind each emitted type was declared with, so docs can group a Types section and build api/types/<category>/<type> routes (the FDR contract from fern-platform#14190, GraphQlTypeCategory = object | input | enum | scalar | interface | union).

The kind cannot live inside the FDR TypeDefinition — that shape is shared with OpenAPI and gRPC, is owned by fern-platform, and is lossy for GraphQL (an object and an interface both convert to object, a custom scalar to alias). So it travels beside the types:

export interface GraphQLConverterResult {
    graphqlOperations: Record<GraphQlOperationId, GraphQlOperation>;
    types: Record<TypeId, TypeDefinition>;
    typeCategories: Record<TypeId, GraphQlTypeCategory>; // new, keyed exactly like `types`
}

Nav emission (one graphqlType node per type, grouped by category) is deliberately not here: the CLI writes the V1 navigation model, which has no GraphQlTypeNode until fern-platform#14190 ships a new @fern-api/fdr-sdk. This PR is the half that is unblocked today.

Changes Made

  • GraphQLConverterResult gains typeCategories, populated in collectTypeDefinitions under the same namespaced TypeId used for types, so a consumer can look up a category for any type it renders without a fallback.
  • Category is derived from the declared GraphQL kind (GraphQLObjectType, GraphQLInputObjectType, …) rather than from the converted FDR shape, with an assertNever tail so a new kind is a compile error instead of a mislabeled page.
  • Query/Mutation roots and built-in scalars keep being skipped, so they get no category.
  • @fern-api/core-utils added as a dependency (for assertNever).
  • Updated README.md generator (if applicable)

Testing

  • Unit tests added/updated — new type-categories fixture exercising all six kinds (the existing GraphQL fixtures cover neither input nor scalar), including an orphan interface with no implementors: the case that proves the category comes from the declared kind and not the shape. The full map is asserted, plus that its keys match types exactly and that namespacing applies to both.
  • pnpm turbo run test --filter @fern-api/graphql-to-fdr — 44 passed; existing snapshots updated for the new field.
  • pnpm turbo run compile --filter @fern-api/graphql-to-fdr, pnpm lint:biome, pnpm format.

Link to Devin session: https://app.devin.ai/sessions/e73e4cb2bda942aa862ef0d8e2a1db48


Open in Devin Review

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

Adds a typeCategories map to GraphQLConverterResult derived from the declared GraphQL kind, with a fixture and snapshot updates. Logic is straightforward; two concerns: instanceof + assertNever can throw at runtime under duplicate graphql installs, and the category is recorded before the type is guaranteed to be emitted into types, which can break the documented "keyed exactly like types" invariant.

  • 🟡 1 warning(s)
  • 🔵 1 suggestion(s)

To request another review, comment /ai-review on this pull request.

Comment on lines +391 to +411
private typeCategoryOf(type: GraphQLNamedType): FernNavigation.GraphQlTypeCategory {
if (type instanceof GraphQLObjectType) {
return "object";
}
if (type instanceof GraphQLInputObjectType) {
return "input";
}
if (type instanceof GraphQLEnumType) {
return "enum";
}
if (type instanceof GraphQLInterfaceType) {
return "interface";
}
if (type instanceof GraphQLUnionType) {
return "union";
}
if (type instanceof GraphQLScalarType) {
return "scalar";
}
assertNever(type);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 warning

instanceof checks against graphql-js classes are unreliable when more than one copy of graphql ends up in the dependency tree (a classic pnpm/monorepo hazard) — that's precisely why graphql-js exports isObjectType, isInputObjectType, isEnumType, isInterfaceType, isUnionType, isScalarType, which check the internal symbol tag instead. Here the failure mode is worse than elsewhere in the file: falling through hits assertNever, which throws and kills the whole conversion instead of just mislabeling a type.

Consider switching to the predicates (they narrow just as well, so assertNever still gives you the exhaustiveness guarantee), or at minimum degrade gracefully rather than throwing on an unrecognized kind.

}

const typeId = this.getNamespacedTypeId(typeName);
this.typeCategories[typeId] = this.typeCategoryOf(type);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

The category is written before any of the kind-specific branches run, so if a branch bails out (recursion guard, unsupported/skipped conversion) you get a typeCategories key with no corresponding types entry — breaking the "keyed exactly like types" contract the docstring and tests promise. Safer to set the category at the same point each branch writes this.types[typeId], or to assert/derive it from this.types keys at the end of convert().

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-27T06:02:31Z).

Fixture main PR Delta
docs 247.0s (n=5) 231.0s (35 versions) -16.0s (-6.5%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-08-27T06:02:31Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-27 11:26 UTC

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-27T06:02:31Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 75s (n=5) 113s (n=5) 64s -11s (-14.7%)
go-sdk square 136s (n=5) 264s (n=5) 141s +5s (+3.7%)
java-sdk square 222s (n=5) 287s (n=5) 176s -46s (-20.7%)
php-sdk square 68s (n=5) N/A 74s +6s (+8.8%)
python-sdk square 152s (n=5) 215s (n=5) 159s +7s (+4.6%)
ruby-sdk-v2 square 109s (n=5) 129s (n=5) 109s +0s (+0.0%)
rust-sdk square 233s (n=5) 217s (n=5) 224s -9s (-3.9%)
swift-sdk square 66s (n=5) 474s (n=5) 78s +12s (+18.2%)
ts-sdk square 160s (n=5) 165s (n=5) 141s -19s (-11.9%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-08-27T06:02:31Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-27 11:27 UTC

rishabh-fern and others added 4 commits August 26, 2026 15:49
Threads the typeCategories map from GraphQLConverter through DocsDefinitionResolver into ApiReferenceNodeConverter, which now emits one graphqlType nav node per named type under a per-kind section (Objects, Inputs, Enums, Scalars, Interfaces, Unions). Kinds the schema does not declare produce no section.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`#convertApiDefinitionPackage` recurses into subpackages before reaching
the GraphQL types block, so the "first call wins" flag handed the type
sections to whichever subpackage was converted first. An API section with
both an OpenAPI spec and a GraphQL schema rendered them nested under an
unrelated REST tag, at `<api>/<tag>/types/<kind>/<type>` instead of the
documented `<api>/types/<kind>/<type>`, while Queries and Mutations
correctly stayed at the root.

Emission is now keyed off the root package itself rather than call order,
and the per-kind groups are collected under a single "GraphQL Types"
section: types belong to the schema rather than to any one package, and
every GraphQL spec in the API section contributes to that one section.
Child slugs are unchanged, so the type-page URLs stay as documented.

Adds a `graphql-type-navigation-subpackages` fixture — the same schema
plus an OpenAPI spec whose tags become subpackages — which is the shape
the existing single-spec fixture could not express. Verified the new test
fails against the previous implementation.

Co-Authored-By: Claude <noreply@anthropic.com>
…nts clauses

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant