Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/cli/api-importers/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dependencies": {
"@fern-api/fdr-sdk": "catalog:",
"@fern-api/fs-utils": "workspace:*",
"@fern-api/ir-sdk": "workspace:*",
"@fern-api/task-context": "workspace:*",
"graphql": "catalog:"
},
Expand Down
116 changes: 116 additions & 0 deletions packages/cli/api-importers/graphql/src/GraphQLToIRConverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { AbsoluteFilePath } from "@fern-api/fs-utils";
import { IntermediateRepresentation } from "@fern-api/ir-sdk";
import { TaskContext } from "@fern-api/task-context";
import { readFile } from "fs/promises";
import { buildSchema, GraphQLSchema } from "graphql";
import { convertGraphQLTypes } from "./ir-conversion/convertGraphQLTypes.js";

/**
* Converts a GraphQL schema into Fern's IntermediateRepresentation.
*
* Each root field on Query/Mutation/Subscription becomes a synthetic HttpEndpoint
* with Transport.graphql, typed variables as the request body, and the fully-resolved
* return type as the response.
*
* This is analogous to ProtobufIRGenerator but operates on GraphQL schemas instead
* of protobuf files.
*/
export class GraphQLToIRConverter {
private context: TaskContext;
private filePath: AbsoluteFilePath;
private namespace: string | undefined;
private schema: GraphQLSchema | undefined;

constructor({
context,
filePath,
namespace
}: { context: TaskContext; filePath: AbsoluteFilePath; namespace?: string }) {
this.context = context;
this.filePath = filePath;
this.namespace = namespace;
}

/**
* Parses the GraphQL schema and produces a complete IntermediateRepresentation
* containing:
* - Types: all schema object/enum/union/input types converted to IR TypeDeclarations
* - Services: one HttpService per operation group (Query, Mutation, Subscription),
* each containing HttpEndpoints for root fields
* - Each endpoint has Transport.graphql with a pre-built query string
*
* @returns IntermediateRepresentation ready to be merged via mergeIntermediateRepresentation()
*/
public async convert(): Promise<IntermediateRepresentation> {
const sdlContent = await readFile(this.filePath, "utf-8");
this.schema = buildSchema(sdlContent);

// TODO: Step 1 - Convert all schema types to IR TypeDeclarations
// Uses convertGraphQLTypes() to produce Record<TypeId, TypeDeclaration>
// Handles: objects, enums, unions, interfaces, input objects, scalars
const types = convertGraphQLTypes({
schema: this.schema,
namespace: this.namespace
});

// TODO: Step 2 - Convert root fields to HttpServices with HttpEndpoints
// Each Query/Mutation/Subscription root type becomes an HttpService
// Each field on those root types becomes an HttpEndpoint with:
// - method: POST, path: /graphql
// - requestBody: typed variables from field arguments
// - response: the field's return type (fully resolved)
// - transport: Transport.graphql({ query, operationType, operationName })
const services = this.convertRootTypesToServices();

// TODO: Step 3 - Assemble the IntermediateRepresentation
// Fill in all required fields (many with sensible defaults for a GraphQL source)
return this.assembleIR({ types, services });
}

/**
* Converts Query, Mutation, and Subscription root types into HttpServices.
* Handles namespace detection (arg-less fields returning "namespace types"
* whose fields are the real operations).
*/
private convertRootTypesToServices(): Record<string, unknown> {
if (!this.schema) {
return {};
}

// TODO: Implement root type → service conversion
// 1. Get Query/Mutation/Subscription root types
// 2. For each root type, detect namespace types (reuse isNamespaceType logic from GraphQLConverter)
// 3. For each root field (or namespace sub-field), call convertRootFieldToEndpoint()
// 4. Group endpoints into HttpService objects
// 5. Return Record<ServiceId, HttpService>

const queryType = this.schema.getQueryType();
const mutationType = this.schema.getMutationType();
const subscriptionType = this.schema.getSubscriptionType();

// TODO: Process each root type
void queryType;
void mutationType;
void subscriptionType;

return {};
}

/**
* Assembles a minimal IntermediateRepresentation from converted types and services.
* Sets sensible defaults for fields not applicable to GraphQL sources.
*/
private assembleIR(_args: {
types: Record<string, unknown>;
services: Record<string, unknown>;
}): IntermediateRepresentation {
// TODO: Construct a valid IntermediateRepresentation with:
// - types: converted GraphQL types
// - services: converted root field services
// - auth: ApiAuth.none (GraphQL auth is typically header-based, handled separately)
// - rootPackage: a Package with service references
// - subpackages: one per service group
// - All other fields: sensible defaults (empty arrays, undefined, etc.)
throw new Error("GraphQLToIRConverter.assembleIR() not yet implemented");
}
}
5 changes: 5 additions & 0 deletions packages/cli/api-importers/graphql/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export type { GraphQLConverterResult, GraphQlExampleInput, GraphQlOperationExamplesInput } from "./GraphQLConverter.js";
export { GraphQLConverter } from "./GraphQLConverter.js";

// GraphQL → IR conversion (for SDK generation)
export { GraphQLToIRConverter } from "./GraphQLToIRConverter.js";
export type { QueryGenerationConfig } from "./query-generation/index.js";
export { generateSelectionQuery } from "./query-generation/index.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLObjectType,
GraphQLScalarType,
GraphQLSchema,
GraphQLUnionType
} from "graphql";

/**
* Converts all user-defined types in a GraphQL schema to IR TypeDeclarations.
*
* Handles:
* - Object types → IR object type with properties
* - Enum types → IR enum type with values
* - Union types → IR undiscriminated union
* - Interface types → IR object type (with extended properties from implementors)
* - Input object types → IR object type (used for request variable types)
* - Custom scalars → IR alias to primitive (string by default)
*
* Skips:
* - Built-in scalars (String, Int, Float, Boolean, ID)
* - Introspection types (__Type, __Field, etc.)
* - Root types (Query, Mutation, Subscription) — these become services, not types
* - Namespace types (arg-less fields whose return types group operations)
*
* @param schema - The parsed GraphQL schema
* @param namespace - Optional namespace prefix for type IDs (for multi-schema support)
* @returns Record<TypeId, TypeDeclaration> ready for the IR
*/
export function convertGraphQLTypes({
schema,
namespace
}: {
schema: GraphQLSchema;
namespace: string | undefined;
}): Record<string, unknown> {
// TODO: Implement type conversion
//
// High-level steps:
// 1. Iterate schema.getTypeMap()
// 2. Skip built-in/introspection types (names starting with "__")
// 3. Skip root types (Query, Mutation, Subscription)
// 4. Skip namespace types (detected via isNamespaceType heuristic)
// 5. For each remaining type, convert based on its kind:
// - GraphQLObjectType → convertObjectType()
// - GraphQLEnumType → convertEnumType()
// - GraphQLUnionType → convertUnionType()
// - GraphQLInterfaceType → convertInterfaceType()
// - GraphQLInputObjectType → convertInputObjectType()
// - GraphQLScalarType → convertCustomScalar()
//
// NOTE: This reuses logic from the existing GraphQLConverter's collectTypeDefinitions(),
// but produces IR TypeDeclarations (from @fern-api/ir-sdk) instead of FDR types.

void schema;
void namespace;
return {};
}

/**
* Converts a GraphQL object type to an IR TypeDeclaration.
* Each field becomes a property with its corresponding IR TypeReference.
*/
function convertObjectType(_type: GraphQLObjectType, _namespace: string | undefined): unknown {
// TODO: Implement
// - Map each field to an ObjectProperty with:
// - name (with casings)
// - valueType: convertOutputTypeToTypeReference(field.type)
// - docs: field.description
// - Handle field arguments (rare for non-root fields, but valid in GraphQL)
return undefined;
}

/**
* Converts a GraphQL enum type to an IR TypeDeclaration.
*/
function convertEnumType(_type: GraphQLEnumType, _namespace: string | undefined): unknown {
// TODO: Implement
// - Map each enum value to an EnumValue with name and docs
return undefined;
}

/**
* Converts a GraphQL union type to an IR TypeDeclaration.
* Uses __typename as the discriminant field.
*/
function convertUnionType(_type: GraphQLUnionType, _namespace: string | undefined): unknown {
// TODO: Implement
// - Each union member becomes a variant
// - __typename is the discriminant
return undefined;
}

/**
* Converts a GraphQL interface type to an IR TypeDeclaration.
*/
function convertInterfaceType(_type: GraphQLInterfaceType, _namespace: string | undefined): unknown {
// TODO: Implement
// - Convert interface fields as base properties
// - May also generate discriminated union for implementing types
return undefined;
}

/**
* Converts a GraphQL input object type to an IR TypeDeclaration.
* Input types are used for request variable types (operation arguments).
*/
function convertInputObjectType(_type: GraphQLInputObjectType, _namespace: string | undefined): unknown {
// TODO: Implement
// - Map each input field to an ObjectProperty
// - Handles nested input objects, lists, enums, scalars
return undefined;
}

/**
* Converts a custom GraphQL scalar to an IR TypeDeclaration.
* Maps known scalars (DateTime, JSON, etc.) to appropriate primitives.
* Unknown custom scalars default to string.
*/
function convertCustomScalar(_type: GraphQLScalarType, _namespace: string | undefined): unknown {
// TODO: Implement
// Known scalar mappings:
// DateTime/Date → string (with format hint)
// JSON/JSONObject → unknown/map
// BigInt/Long → long
// URL/URI → string
// Everything else → string (safest default)
return undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { GraphQLField, GraphQLSchema } from "graphql";
import { generateSelectionQuery, QueryGenerationConfig } from "../query-generation/generateSelectionQuery.js";

/**
* Converts a single GraphQL root field into a synthetic HttpEndpoint for the IR.
*
* A root field like `user(id: ID!): User` on Query becomes:
* - method: POST
* - path: /graphql
* - requestBody: typed variables object { id: string }
* - response: User (fully resolved type)
* - transport: Transport.graphql({ query: "query user($id: ID!) { user(id: $id) { ... } }", operationType: "QUERY", operationName: "user" })
*
* @param field - The root field to convert (from Query/Mutation/Subscription)
* @param operationType - "QUERY" | "MUTATION" | "SUBSCRIPTION"
* @param schema - The full schema (for query generation and type resolution)
* @param namespace - Optional namespace prefix for service/endpoint IDs
* @param config - Query generation depth config
* @returns An HttpEndpoint-shaped object ready for inclusion in an HttpService
*/
export function convertRootFieldToEndpoint({
field,
operationType,
schema,
namespace,
config
}: {
field: GraphQLField<unknown, unknown>;
operationType: "QUERY" | "MUTATION" | "SUBSCRIPTION";
schema: GraphQLSchema;
namespace: string | undefined;
config: QueryGenerationConfig;
}): unknown {
// TODO: Implement endpoint conversion
//
// Steps:
// 1. Generate the pre-built query string using generateSelectionQuery()
const _queryString = generateSelectionQuery(
field,
schema,
operationType.toLowerCase() as "query" | "mutation" | "subscription",
config
);

// 2. Convert field.args → request body type (variables object)
// - Each arg becomes a property on a synthetic object type
// - Required args (NonNull without default) → required properties
// - Optional args → optional properties
// - The synthetic type is named `${FieldName}Variables`
const _variablesType = convertFieldArgsToVariablesType(field, namespace);

// 3. Resolve the return type to an IR TypeReference
// - Unwrap NonNull/List wrappers
// - Map to existing TypeDeclaration by name
const _responseType = resolveReturnType(field);

// 4. Build the HttpEndpoint object:
// {
// id: EndpointId (e.g., "endpoint_query_user")
// name: Name with casings
// method: "POST"
// headers: [] (auth headers handled separately)
// path: { head: "/graphql", parts: [] }
// queryParameters: []
// requestBody: InlinedRequestBody with variables properties
// response: JsonResponse with unwrapped return type
// transport: Transport.graphql({ query: queryString, operationType, operationName: field.name })
// sdkRequest, auth, errors, examples, etc.: defaults
// }

// 5. Return the HttpEndpoint
return undefined;
}

/**
* Converts a root field's arguments into a synthetic "Variables" type.
* This becomes the request body shape for the endpoint.
*
* Example: `user(id: ID!, includeDeleted: Boolean)` →
* {
* properties: [
* { name: "id", valueType: TypeReference.primitive(string), required: true },
* { name: "includeDeleted", valueType: TypeReference.primitive(boolean), required: false }
* ]
* }
*/
function convertFieldArgsToVariablesType(
_field: GraphQLField<unknown, unknown>,
_namespace: string | undefined
): unknown {
// TODO: Implement
// For each argument:
// - name: arg.name (with casings)
// - valueType: convertInputTypeToTypeReference(arg.type)
// - required: isNonNull(arg.type) && arg.defaultValue === undefined
// - docs: arg.description
return undefined;
}

/**
* Resolves a field's return type to an IR TypeReference.
*
* Unwraps NonNull/List wrappers and maps to:
* - Named types → TypeReference.named(typeId)
* - Lists → TypeReference.container(list(...))
* - Scalars → TypeReference.primitive(...)
*/
function resolveReturnType(_field: GraphQLField<unknown, unknown>): unknown {
// TODO: Implement
// 1. Unwrap GraphQLNonNull wrapper (note optionality for the outer level)
// 2. If List → recurse on inner type, wrap in TypeReference.container.list()
// 3. If named type → look up by name, return TypeReference.named(typeId)
// 4. If scalar → map to primitive TypeReference
return undefined;
}
2 changes: 2 additions & 0 deletions packages/cli/api-importers/graphql/src/ir-conversion/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { convertGraphQLTypes } from "./convertGraphQLTypes.js";
export { convertRootFieldToEndpoint } from "./convertRootFieldToEndpoint.js";
Loading