Skip to content

Commit

Permalink
Break up circular refs in GraphQL
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-lee committed Jan 24, 2025
1 parent 700098f commit 69fb1f9
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 11 deletions.
29 changes: 29 additions & 0 deletions packages/zudoku/src/lib/oas/graphql/circular.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { GraphQLJSON } from "graphql-type-json";
import { GraphQLScalarType } from "graphql/index.js";

export const CIRCULAR_REF = "$[Circular Reference]";
const handleCircularRefs = (obj: any, visited = new WeakSet()): any => {
if (obj === CIRCULAR_REF) return CIRCULAR_REF;
if (obj === null || typeof obj !== "object") return obj;

if (visited.has(obj)) return CIRCULAR_REF;

visited.add(obj);

if (Array.isArray(obj)) {
return obj.map((item) => handleCircularRefs(item, visited));
}

const result: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = handleCircularRefs(value, visited);
}
return result;
};
export const GraphQLJSONSchema = new GraphQLScalarType({
...GraphQLJSON,
name: "JSONSchema",
description: "OpenAPI schema scalar type that handles circular references",
serialize: (value: unknown) =>
GraphQLJSON.serialize(handleCircularRefs(value)),
});
18 changes: 9 additions & 9 deletions packages/zudoku/src/lib/oas/graphql/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import SchemaBuilder from "@pothos/core";
import {
slugifyWithCounter,
type CountableSlugify,
slugifyWithCounter,
} from "@sindresorhus/slugify";
import { GraphQLJSON, GraphQLJSONObject } from "graphql-type-json";
import { createYoga, type YogaServerOptions } from "graphql-yoga";
import {
HttpMethods,
validate,
type EncodingObject,
type ExampleObject,
HttpMethods,
type OpenAPIDocument,
type OperationObject,
type ParameterObject,
type PathsObject,
type SchemaObject,
type ServerObject,
type TagObject,
validate,
} from "../parser/index.js";
import { GraphQLJSONSchema } from "./circular.js";

export type {
EncodingObject,
Expand All @@ -45,10 +46,7 @@ export const createOperationSlug = (
) => {
const summary =
(operation.summary ?? "") +
(operation.operationId
? "-" +
operation.operationId.slice(0, operation.summary ? Infinity : Infinity)
: "");
(operation.operationId ? "-" + operation.operationId : "");

return slugify(
(tag ? tag + "-" : "") +
Expand All @@ -65,6 +63,7 @@ const builder = new SchemaBuilder<{
Scalars: {
JSON: any;
JSONObject: any;
JSONSchema: any;
};
Context: {
schema: OpenAPIDocument;
Expand All @@ -74,6 +73,7 @@ const builder = new SchemaBuilder<{

const JSONScalar = builder.addScalarType("JSON", GraphQLJSON);
const JSONObjectScalar = builder.addScalarType("JSONObject", GraphQLJSONObject);
const JSONSchemaScalar = builder.addScalarType("JSONSchema", GraphQLJSONSchema);

const getAllTags = (schema: OpenAPIDocument): TagObject[] => {
const tags = schema.tags ?? [];
Expand Down Expand Up @@ -238,7 +238,7 @@ const ParameterItem = builder
})),
nullable: true,
}),
schema: t.expose("schema", { type: JSONScalar, nullable: true }),
schema: t.expose("schema", { type: JSONSchemaScalar, nullable: true }),
}),
});

Expand All @@ -252,7 +252,7 @@ const MediaTypeItem = builder
.implement({
fields: (t) => ({
mediaType: t.exposeString("mediaType"),
schema: t.expose("schema", { type: JSONScalar, nullable: true }),
schema: t.expose("schema", { type: JSONSchemaScalar, nullable: true }),
examples: t.expose("examples", { type: [ExampleItem], nullable: true }),
encoding: t.expose("encoding", { type: [EncodingItem], nullable: true }),
}),
Expand Down
3 changes: 1 addition & 2 deletions packages/zudoku/src/lib/oas/parser/dereference/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { JSONSchema4, JSONSchema6 } from "json-schema";
import { CIRCULAR_REF } from "../../graphql/circular.js";
import { resolveLocalRef } from "./resolveRef.js";

export type JSONSchema = JSONSchema4 | JSONSchema6;

export const CIRCULAR_REF = "$[Circular Reference]";

type CustomResolver = (ref: string) => Promise<JSONSchema | undefined>;

const cache = new Map<JSONSchema, JSONSchema>();
Expand Down

0 comments on commit 69fb1f9

Please sign in to comment.