import { describe, it, expect } from "@effect/vitest";import { Effect, Option } from "effect"; import { extract } from "./extract";import type { IntrospectionResult } from "./introspect"; // ---------------------------------------------------------------------------// Minimal introspection fixture// --------------------------------------------------------------------------- const makeIntrospection = ( overrides?: Partial<IntrospectionResult["__schema"]>,): IntrospectionResult => ({ __schema: { queryType: { name: "Query" }, mutationType: { name: "Mutation" }, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "user", description: "Fetch a user by ID", args: [ { name: "id", description: "User ID", type: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "ID", ofType: null }, }, defaultValue: null, }, ], type: { kind: "OBJECT", name: "User", ofType: null }, }, { name: "users", description: "List all users", args: [], type: { kind: "NON_NULL", name: null, ofType: { kind: "LIST", name: null, ofType: { kind: "OBJECT", name: "User", ofType: null }, }, }, }, ], inputFields: null, enumValues: null, }, { kind: "OBJECT", name: "Mutation", description: null, fields: [ { name: "createUser", description: "Create a new user", args: [ { name: "name", description: null, type: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null }, }, defaultValue: null, }, { name: "email", description: null, type: { kind: "SCALAR", name: "String", ofType: null }, defaultValue: null, }, ], type: { kind: "OBJECT", name: "User", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "OBJECT", name: "User", description: null, fields: [ { name: "id", description: null, args: [], type: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "ID", ofType: null }, }, }, { name: "name", description: null, args: [], type: { kind: "SCALAR", name: "String", ofType: null }, }, { name: "email", description: null, args: [], type: { kind: "SCALAR", name: "String", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "ID", description: null, fields: null, inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "Boolean", description: null, fields: null, inputFields: null, enumValues: null, }, ], ...overrides, },}); // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- describe("extract", () => { it("extracts queries and mutations from introspection result", async () => { const { result } = await Effect.runPromise(extract(makeIntrospection())); const queries = result.fields.filter((f) => f.kind === "query"); const mutations = result.fields.filter((f) => f.kind === "mutation"); expect(queries).toHaveLength(2); expect(mutations).toHaveLength(1); const userQuery = queries.find((f) => f.fieldName === "user"); expect(userQuery).toBeDefined(); expect(userQuery!.arguments).toHaveLength(1); expect(userQuery!.arguments[0].name).toBe("id"); expect(userQuery!.arguments[0].required).toBe(true); expect(userQuery!.returnTypeName).toBe("User"); const createMutation = mutations.find((f) => f.fieldName === "createUser"); expect(createMutation).toBeDefined(); expect(createMutation!.arguments).toHaveLength(2); expect(createMutation!.arguments[0].name).toBe("name"); expect(createMutation!.arguments[0].required).toBe(true); expect(createMutation!.arguments[1].name).toBe("email"); expect(createMutation!.arguments[1].required).toBe(false); }); it("builds input schema with required fields", async () => { const { result } = await Effect.runPromise(extract(makeIntrospection())); const createUser = result.fields.find((f) => f.fieldName === "createUser"); expect(createUser).toBeDefined(); expect(Option.isSome(createUser!.inputSchema)).toBe(true); const schema = Option.getOrThrow(createUser!.inputSchema) as Record<string, unknown>; expect(schema.type).toBe("object"); const properties = schema.properties as Record<string, Record<string, unknown>>; expect(properties.name.type).toBe("string"); expect(schema.required).toContain("name"); expect(schema.required).not.toContain("email"); }); it("handles schema with no mutations", async () => { const { result } = await Effect.runPromise(extract(makeIntrospection({ mutationType: null }))); const mutations = result.fields.filter((f) => f.kind === "mutation"); expect(mutations).toHaveLength(0); }); it("handles empty query type", async () => { const { result } = await Effect.runPromise(extract(makeIntrospection({ queryType: null }))); const queries = result.fields.filter((f) => f.kind === "query"); expect(queries).toHaveLength(0); }); it("handles circular input type references without stack overflow", async () => { const circular: IntrospectionResult = { __schema: { queryType: { name: "Query" }, mutationType: null, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "issues", description: "List issues", args: [ { name: "filter", description: null, type: { kind: "INPUT_OBJECT", name: "IssueFilter", ofType: null }, defaultValue: null, }, ], type: { kind: "OBJECT", name: "Issue", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "INPUT_OBJECT", name: "IssueFilter", description: null, fields: null, inputFields: [ { name: "title", description: null, type: { kind: "SCALAR", name: "String", ofType: null }, defaultValue: null, }, { name: "and", description: "Compound filter", type: { kind: "LIST", name: null, ofType: { kind: "INPUT_OBJECT", name: "IssueFilter", ofType: null }, }, defaultValue: null, }, { name: "or", description: null, type: { kind: "LIST", name: null, ofType: { kind: "INPUT_OBJECT", name: "IssueFilter", ofType: null }, }, defaultValue: null, }, ], enumValues: null, }, { kind: "OBJECT", name: "Issue", description: null, fields: [ { name: "id", description: null, args: [], type: { kind: "SCALAR", name: "ID", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "ID", description: null, fields: null, inputFields: null, enumValues: null, }, ], }, }; // Should not throw "Maximum call stack size exceeded" const { result, definitions } = await Effect.runPromise(extract(circular)); expect(result.fields).toHaveLength(1); const issuesField = result.fields[0]; expect(issuesField.fieldName).toBe("issues"); // The filter arg should use a $ref, not inline the full type const schema = Option.getOrThrow(issuesField.inputSchema) as Record<string, unknown>; const schemaProps = schema.properties as Record<string, Record<string, unknown>>; expect(schemaProps.filter.$ref).toBe("#/$defs/IssueFilter"); // The definition should exist with proper fields const filterDef = definitions["IssueFilter"] as Record<string, unknown>; expect(filterDef.type).toBe("object"); const filterProps = filterDef.properties as Record<string, Record<string, unknown>>; expect(filterProps.title.type).toBe("string"); // Self-referential "and" field uses $ref back to itself expect(filterProps.and.type).toBe("array"); const andItems = filterProps.and.items as Record<string, unknown>; expect(andItems.$ref).toBe("#/$defs/IssueFilter"); });});
import { describe, it, expect } from "@effect/vitest";
import { Effect, Option } from "effect";
import { extract } from "./extract";
import type { IntrospectionResult } from "./introspect";
// ---------------------------------------------------------------------------
// Minimal introspection fixture
// ---------------------------------------------------------------------------
const makeIntrospection = (
overrides?: Partial<IntrospectionResult["__schema"]>,
): IntrospectionResult => ({
__schema: {
queryType: { name: "Query" },
mutationType: { name: "Mutation" },
types: [
{
kind: "OBJECT",
name: "Query",
description: null,
fields: [
{
name: "user",
description: "Fetch a user by ID",
args: [
{
name: "id",
description: "User ID",
type: {
kind: "NON_NULL",
name: null,
ofType: { kind: "SCALAR", name: "ID", ofType: null },
},
defaultValue: null,
},
],
type: { kind: "OBJECT", name: "User", ofType: null },
},
{
name: "users",
description: "List all users",
args: [],
type: {
kind: "NON_NULL",
name: null,
ofType: {
kind: "LIST",
name: null,
ofType: { kind: "OBJECT", name: "User", ofType: null },
},
},
},
],
inputFields: null,
enumValues: null,
},
{
kind: "OBJECT",
name: "Mutation",
description: null,
fields: [
{
name: "createUser",
description: "Create a new user",
args: [
{
name: "name",
description: null,
type: {
kind: "NON_NULL",
name: null,
ofType: { kind: "SCALAR", name: "String", ofType: null },
},
defaultValue: null,
},
{
name: "email",
description: null,
type: { kind: "SCALAR", name: "String", ofType: null },
defaultValue: null,
},
],
type: { kind: "OBJECT", name: "User", ofType: null },
},
],
inputFields: null,
enumValues: null,
},
{
kind: "OBJECT",
name: "User",
description: null,
fields: [
{
name: "id",
description: null,
args: [],
type: {
kind: "NON_NULL",
name: null,
ofType: { kind: "SCALAR", name: "ID", ofType: null },
},
},
{
name: "name",
description: null,
args: [],
type: { kind: "SCALAR", name: "String", ofType: null },
},
{
name: "email",
description: null,
args: [],
type: { kind: "SCALAR", name: "String", ofType: null },
},
],
inputFields: null,
enumValues: null,
},
{
kind: "SCALAR",
name: "ID",
description: null,
fields: null,
inputFields: null,
enumValues: null,
},
{
kind: "SCALAR",
name: "String",
description: null,
fields: null,
inputFields: null,
enumValues: null,
},
{
kind: "SCALAR",
name: "Boolean",
description: null,
fields: null,
inputFields: null,
enumValues: null,
},
],
...overrides,
},
});
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("extract", () => {
it("extracts queries and mutations from introspection result", async () => {
const { result } = await Effect.runPromise(extract(makeIntrospection()));
const queries = result.fields.filter((f) => f.kind === "query");
const mutations = result.fields.filter((f) => f.kind === "mutation");
expect(queries).toHaveLength(2);
expect(mutations).toHaveLength(1);
const userQuery = queries.find((f) => f.fieldName === "user");
expect(userQuery).toBeDefined();
expect(userQuery!.arguments).toHaveLength(1);
expect(userQuery!.arguments[0].name).toBe("id");
expect(userQuery!.arguments[0].required).toBe(true);
expect(userQuery!.returnTypeName).toBe("User");
const createMutation = mutations.find((f) => f.fieldName === "createUser");
expect(createMutation).toBeDefined();
expect(createMutation!.arguments).toHaveLength(2);
expect(createMutation!.arguments[0].name).toBe("name");
expect(createMutation!.arguments[0].required).toBe(true);
expect(createMutation!.arguments[1].name).toBe("email");
expect(createMutation!.arguments[1].required).toBe(false);
});
it("builds input schema with required fields", async () => {
const { result } = await Effect.runPromise(extract(makeIntrospection()));
const createUser = result.fields.find((f) => f.fieldName === "createUser");
expect(createUser).toBeDefined();
expect(Option.isSome(createUser!.inputSchema)).toBe(true);
const schema = Option.getOrThrow(createUser!.inputSchema) as Record<string, unknown>;
expect(schema.type).toBe("object");
const properties = schema.properties as Record<string, Record<string, unknown>>;
expect(properties.name.type).toBe("string");
expect(schema.required).toContain("name");
expect(schema.required).not.toContain("email");
});
it("handles schema with no mutations", async () => {
const { result } = await Effect.runPromise(extract(makeIntrospection({ mutationType: null })));
const mutations = result.fields.filter((f) => f.kind === "mutation");
expect(mutations).toHaveLength(0);
});
it("handles empty query type", async () => {
const { result } = await Effect.runPromise(extract(makeIntrospection({ queryType: null })));
const queries = result.fields.filter((f) => f.kind === "query");
expect(queries).toHaveLength(0);
});
it("handles circular input type references without stack overflow", async () => {
const circular: IntrospectionResult = {
__schema: {
queryType: { name: "Query" },
mutationType: null,
types: [
{
kind: "OBJECT",
name: "Query",
description: null,
fields: [
{
name: "issues",
description: "List issues",
args: [
{
name: "filter",
description: null,
type: { kind: "INPUT_OBJECT", name: "IssueFilter", ofType: null },
defaultValue: null,
},
],
type: { kind: "OBJECT", name: "Issue", ofType: null },
},
],
inputFields: null,
enumValues: null,
},
{
kind: "INPUT_OBJECT",
name: "IssueFilter",
description: null,
fields: null,
inputFields: [
{
name: "title",
description: null,
type: { kind: "SCALAR", name: "String", ofType: null },
defaultValue: null,
},
{
name: "and",
description: "Compound filter",
type: {
kind: "LIST",
name: null,
ofType: { kind: "INPUT_OBJECT", name: "IssueFilter", ofType: null },
},
defaultValue: null,
},
{
name: "or",
description: null,
type: {
kind: "LIST",
name: null,
ofType: { kind: "INPUT_OBJECT", name: "IssueFilter", ofType: null },
},
defaultValue: null,
},
],
enumValues: null,
},
{
kind: "OBJECT",
name: "Issue",
description: null,
fields: [
{
name: "id",
description: null,
args: [],
type: { kind: "SCALAR", name: "ID", ofType: null },
},
],
inputFields: null,
enumValues: null,
},
{
kind: "SCALAR",
name: "String",
description: null,
fields: null,
inputFields: null,
enumValues: null,
},
{
kind: "SCALAR",
name: "ID",
description: null,
fields: null,
inputFields: null,
enumValues: null,
},
],
},
};
// Should not throw "Maximum call stack size exceeded"
const { result, definitions } = await Effect.runPromise(extract(circular));
expect(result.fields).toHaveLength(1);
const issuesField = result.fields[0];
expect(issuesField.fieldName).toBe("issues");
// The filter arg should use a $ref, not inline the full type
const schema = Option.getOrThrow(issuesField.inputSchema) as Record<string, unknown>;
const schemaProps = schema.properties as Record<string, Record<string, unknown>>;
expect(schemaProps.filter.$ref).toBe("#/$defs/IssueFilter");
// The definition should exist with proper fields
const filterDef = definitions["IssueFilter"] as Record<string, unknown>;
expect(filterDef.type).toBe("object");
const filterProps = filterDef.properties as Record<string, Record<string, unknown>>;
expect(filterProps.title.type).toBe("string");
// Self-referential "and" field uses $ref back to itself
expect(filterProps.and.type).toBe("array");
const andItems = filterProps.and.items as Record<string, unknown>;
expect(andItems.$ref).toBe("#/$defs/IssueFilter");
});
});