328 lines · 10.2 KB · TypeScript
RawDownload
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");  });});