-
Notifications
You must be signed in to change notification settings - Fork 341
feat(cli): record the GraphQL kind of every named type #17542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d7763a6
0110160
d00851f
0ac0e03
b06afa4
180580c
1cdd1a5
7f5ab1d
bba783e
d87ceca
0a4ca38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { FdrAPI } from "@fern-api/fdr-sdk"; | ||
| import { assertNever } from "@fern-api/core-utils"; | ||
| import { FdrAPI, FernNavigation } from "@fern-api/fdr-sdk"; | ||
| import { AbsoluteFilePath } from "@fern-api/fs-utils"; | ||
| import { TaskContext } from "@fern-api/task-context"; | ||
| import { readFile } from "fs/promises"; | ||
|
|
@@ -13,6 +14,7 @@ import { | |
| GraphQLInputType, | ||
| GraphQLInterfaceType, | ||
| GraphQLList, | ||
| GraphQLNamedType, | ||
| GraphQLNonNull, | ||
| GraphQLObjectType, | ||
| GraphQLOutputType, | ||
|
|
@@ -26,6 +28,17 @@ import { mergeGraphQlDocuments } from "./mergeGraphQlDocuments.js"; | |
| export interface GraphQLConverterResult { | ||
| graphqlOperations: Record<FdrAPI.GraphQlOperationId, FdrAPI.api.v1.register.GraphQlOperation>; | ||
| types: Record<FdrAPI.TypeId, FdrAPI.api.v1.register.TypeDefinition>; | ||
| /** | ||
| * The GraphQL kind each documented type was declared with, keyed like `types`. | ||
| * | ||
| * This is the set of types that get a page, so it is a subset of `types`: an operation | ||
| * namespace is left out because its fields are documented as operations, while its definition | ||
| * stays in `types` for the namespace query's page to render. Look up with a null check. | ||
| * | ||
| * The FDR type shape is shared with OpenAPI and gRPC and cannot express a GraphQL kind, so | ||
| * the kind travels alongside the types rather than inside them. | ||
| */ | ||
| typeCategories: Record<FdrAPI.TypeId, FernNavigation.GraphQlTypeCategory>; | ||
| } | ||
|
|
||
| export interface GraphQlExampleInput { | ||
|
|
@@ -55,6 +68,8 @@ export class GraphQLConverter { | |
| private namespace: string | undefined; | ||
| private processingTypes: Set<string> = new Set(); | ||
| private types: Record<FdrAPI.TypeId, FdrAPI.api.v1.register.TypeDefinition> = {}; | ||
| private typeCategories: Record<FdrAPI.TypeId, FernNavigation.GraphQlTypeCategory> = {}; | ||
| private namespaceTypeNames: Set<string> = new Set(); | ||
| private examplesByOperation: Map<string, FdrAPI.api.v1.register.GraphQlExample[]> = new Map(); | ||
|
|
||
| constructor({ | ||
|
|
@@ -235,7 +250,14 @@ export class GraphQLConverter { | |
|
|
||
| const graphqlOperations = this.resolveOperationIds(pendingOperations); | ||
|
|
||
| return { graphqlOperations, types: this.types }; | ||
| // A namespace type's fields are documented as operations, so it groups operations rather | ||
| // than being a documented type: no kind means no type page. Its definition stays in | ||
| // `types` because a namespace query's page renders the nested fields from it. | ||
| for (const typeName of this.namespaceTypeNames) { | ||
| delete this.typeCategories[this.getNamespacedTypeId(typeName)]; | ||
| } | ||
|
|
||
| return { graphqlOperations, types: this.types, typeCategories: this.typeCategories }; | ||
| } | ||
|
|
||
| private resolveOperationIds( | ||
|
|
@@ -289,6 +311,7 @@ export class GraphQLConverter { | |
| } | ||
|
|
||
| const typeId = this.getNamespacedTypeId(typeName); | ||
| this.typeCategories[typeId] = this.typeCategoryOf(type); | ||
|
|
||
| if (type instanceof GraphQLEnumType) { | ||
| this.processingTypes.add(typeName); | ||
|
|
@@ -374,6 +397,31 @@ export class GraphQLConverter { | |
| } | ||
| } | ||
|
|
||
| // Derived from the declared kind, never from the converted FDR shape: an interface and an | ||
| // object both convert to `object` and a custom scalar to `alias`, so shape-sniffing would | ||
| // mislabel types. | ||
| 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); | ||
| } | ||
|
Comment on lines
+403
to
+423
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 warning
Consider switching to the predicates (they narrow just as well, so |
||
|
|
||
| // Builds an operation id of the form `<operationType>_<segments joined by ".">`. | ||
| // Flat (top-level) ids use a single segment; namespaced ids include the full field | ||
| // path so that fields sharing a leaf name across namespaces resolve to distinct ids. | ||
|
|
@@ -409,6 +457,7 @@ export class GraphQLConverter { | |
| operation: this.convertField(field, fieldName, operationType) | ||
| }); | ||
| } | ||
| this.namespaceTypeNames.add(returnRawType.name); | ||
| this.convertNamespaceOperations(returnRawType, operationType, pending, [fieldName]); | ||
| } else { | ||
| const flatId = this.buildOperationId(operationType, [fieldName]); | ||
|
|
@@ -688,21 +737,10 @@ export class GraphQLConverter { | |
| }) | ||
| ); | ||
|
|
||
| // Only extend interfaces that are converted to plain objects (no implementations). | ||
| // Interfaces with implementations are converted to undiscriminatedUnion, and the | ||
| // frontend's unwrapObjectType only supports extending object types. | ||
| // GraphQL implementing types already include all interface fields, so extends is | ||
| // only needed for documentation purposes when the interface is a plain object. | ||
| const interfaces = type.getInterfaces(); | ||
| const extendsIds = interfaces | ||
| .filter((iface) => { | ||
| if (!this.schema) { | ||
| return true; | ||
| } | ||
| const implementations = this.schema.getPossibleTypes(iface); | ||
| return implementations.length === 0; | ||
| }) | ||
| .map((iface) => this.getNamespacedTypeId(iface.name)); | ||
| // `extends` carries the `implements` clause, which is the only edge the docs have to | ||
| // resolve an interface's implementors. Implementing types already inline every interface | ||
| // field, so the extended properties are deduplicated away when rendering. | ||
| const extendsIds = type.getInterfaces().map((iface) => this.getNamespacedTypeId(iface.name)); | ||
|
|
||
| return { | ||
| type: "object", | ||
|
|
@@ -712,33 +750,9 @@ export class GraphQLConverter { | |
| }; | ||
| } | ||
|
|
||
| // An interface is its own set of fields, not the union of the types that implement it: the | ||
| // implementors are reachable from each implementing type's `extends`. | ||
| private convertInterfaceTypeDefinition(type: GraphQLInterfaceType): FdrAPI.api.v1.register.TypeShape { | ||
| if (!this.schema) { | ||
| return this.convertInterfaceAsObject(type); | ||
| } | ||
|
|
||
| const implementations = this.schema.getPossibleTypes(type); | ||
| if (implementations.length === 0) { | ||
| return this.convertInterfaceAsObject(type); | ||
| } | ||
|
|
||
| return { | ||
| type: "undiscriminatedUnion", | ||
| variants: implementations.map((impl) => ({ | ||
| typeName: impl.name, | ||
| displayName: impl.name, | ||
| type: { | ||
| type: "id", | ||
| value: this.getNamespacedTypeId(impl.name), | ||
| default: undefined | ||
| }, | ||
| description: impl.description ?? undefined, | ||
| availability: undefined | ||
| })) | ||
| }; | ||
| } | ||
|
|
||
| private convertInterfaceAsObject(type: GraphQLInterfaceType): FdrAPI.api.v1.register.TypeShape { | ||
| const fields = type.getFields(); | ||
| const properties: FdrAPI.api.v1.register.ObjectProperty[] = Object.entries(fields).map( | ||
| ([fieldName, field]) => ({ | ||
|
|
||
There was a problem hiding this comment.
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
typeCategorieskey with no correspondingtypesentry — breaking the "keyed exactly liketypes" contract the docstring and tests promise. Safer to set the category at the same point each branch writesthis.types[typeId], or to assert/derive it fromthis.typeskeys at the end ofconvert().