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
72 changes: 64 additions & 8 deletions packages/cli/api-importers/graphql/src/GraphQLConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export class GraphQLConverter {
return true;
}

// NOTE: This heuristic treats a type as a namespace (operation-grouping type) when ALL
// of its fields accept arguments. This can produce a false positive for regular data
// types where every field happens to be parameterized. If that becomes a problem,
// introduce an explicit config option (e.g. namespacedRootTypes: [...]) rather than
// relying on field-arg counts alone.
private isNamespaceType(type: GraphQLObjectType): boolean {
const fields = Object.values(type.getFields());
if (fields.length === 0) {
Expand All @@ -113,6 +118,44 @@ export class GraphQLConverter {
return fields.every((f) => f.args.length > 0);
}

// Returns the names of object types that will be consumed as namespace groupings by
// convertOperations — i.e. types that appear as the return type of a zero-arg root
// field whose own fields all accept arguments. These types must be excluded from the
// type registry in collectTypeDefinitions to prevent their fields from showing up
// both as object properties and as standalone operations.
private collectNamespaceTypeNames(): Set<string> {
if (!this.schema) {
return new Set();
}
const namespaceTypeNames = new Set<string>();
const rootTypes: GraphQLObjectType[] = [];
const queryType = this.schema.getQueryType();
if (queryType) {
rootTypes.push(queryType);
}
const mutationType = this.schema.getMutationType();
if (mutationType) {
rootTypes.push(mutationType);
}
const subscriptionType = this.schema.getSubscriptionType();
if (subscriptionType && this.isActualSubscriptionRootType(subscriptionType)) {
rootTypes.push(subscriptionType);
}
for (const rootType of rootTypes) {
for (const field of Object.values(rootType.getFields())) {
const returnRawType = this.unwrapNonNull(field.type);
if (
returnRawType instanceof GraphQLObjectType &&
field.args.length === 0 &&
this.isNamespaceType(returnRawType)
) {
namespaceTypeNames.add(returnRawType.name);
}
}
}
return namespaceTypeNames;
}

public async convert(): Promise<GraphQLConverterResult> {
const sdlContent = await readFile(this.filePath, "utf-8");
this.schema = buildSchema(sdlContent);
Expand Down Expand Up @@ -144,6 +187,12 @@ export class GraphQLConverter {
return;
}

// Identify namespace types up-front so we can exclude them below. Namespace types
// have their fields registered as standalone operations via convertNamespaceOperations;
// registering them as type definitions too would make their fields appear in both
// places, causing redundant/confusing output.
const namespaceTypeNames = this.collectNamespaceTypeNames();

const typeMap = this.schema.getTypeMap();
for (const [typeName, type] of Object.entries(typeMap)) {
// Skip built-in types
Expand All @@ -163,6 +212,11 @@ export class GraphQLConverter {
continue;
}

// Skip namespace types — their fields are promoted to top-level operations.
if (type instanceof GraphQLObjectType && namespaceTypeNames.has(typeName)) {
continue;
}

if (type instanceof GraphQLScalarType && this.isBuiltInScalar(typeName)) {
continue;
}
Expand Down Expand Up @@ -508,13 +562,14 @@ export class GraphQLConverter {

private convertObjectTypeDefinition(type: GraphQLObjectType): FdrAPI.api.v1.register.TypeShape {
const fields = type.getFields();
const properties = Object.entries(fields).map(
([fieldName, field]): FdrAPI.api.v1.register.ObjectProperty => ({
const properties: FdrAPI.api.v1.register.ObjectProperty[] = Object.entries(fields).map(
([fieldName, field]) => ({
key: FdrAPI.PropertyKey(fieldName),
valueType: this.convertOutputType(field.type),
description: field.description ?? undefined,
availability: undefined,
propertyAccess: undefined
propertyAccess: undefined,
arguments: field.args.length > 0 ? field.args.map((arg) => this.convertArgument(arg)) : undefined
})
Comment thread
rishabh-fern marked this conversation as resolved.
Comment thread
claude[bot] marked this conversation as resolved.
);

Expand Down Expand Up @@ -570,13 +625,14 @@ export class GraphQLConverter {

private convertInterfaceAsObject(type: GraphQLInterfaceType): FdrAPI.api.v1.register.TypeShape {
const fields = type.getFields();
const properties = Object.entries(fields).map(
([fieldName, field]): FdrAPI.api.v1.register.ObjectProperty => ({
const properties: FdrAPI.api.v1.register.ObjectProperty[] = Object.entries(fields).map(
([fieldName, field]) => ({
key: FdrAPI.PropertyKey(fieldName),
valueType: this.convertOutputType(field.type),
description: field.description ?? undefined,
availability: undefined,
propertyAccess: undefined
propertyAccess: undefined,
arguments: field.args.length > 0 ? field.args.map((arg) => this.convertArgument(arg)) : undefined
})
);

Expand All @@ -590,8 +646,8 @@ export class GraphQLConverter {

private convertInputObjectTypeDefinition(type: GraphQLInputObjectType): FdrAPI.api.v1.register.TypeShape {
const fields = type.getFields();
const properties = Object.entries(fields).map(
([fieldName, field]): FdrAPI.api.v1.register.ObjectProperty => ({
const properties: FdrAPI.api.v1.register.ObjectProperty[] = Object.entries(fields).map(
([fieldName, field]) => ({
key: FdrAPI.PropertyKey(fieldName),
valueType: this.convertInputType(field.type),
description: field.description ?? undefined,
Expand Down
Loading
Loading