Repository: graphql-markdown/graphql-markdown
Pull request #938 (2023-08-11 10:27:17)

:recycle: refactor printer legacy
# Description

Refactor printer-legacy based on mutation test report

# Checklist

- [x] My changes follow the [contributing guidelines](https://github.com/graphql-markdown/graphql-markdown/blob/main/CONTRIBUTING.md) of this project.
- [x] I have performed a self-review of my code.
- [ ] I have made corresponding changes to the documentation.
- [x] I have added tests that prove my fix is effective or that my changes work.
- [x] New and existing unit tests pass locally with my changes.


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=graphql-markdown_graphql-markdown&pullRequest=938)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=938&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=graphql-markdown_graphql-markdown&pullRequest=938) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=graphql-markdown_graphql-markdown&pullRequest=938&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=graphql-markdown_graphql-markdown&pullRequest=938&metric=new_duplicated_lines_density&view=list)


codesee-maps[bot]: <details open>
<summary><b>👇 <a href="https://app.codesee.io/r/reviews?pr=938&src=https%3A%2F%2Fgithub.com%2Fgraphql-markdown%2Fgraphql-markdown">Click on the image</a> for a new way to code review</b>
</summary>

####

[![Review these changes using an interactive CodeSee Map](https://s3.us-east-2.amazonaws.com/maps.codesee.io/images/github/graphql-markdown/graphql-markdown/938/747a2a59/91e68647c162241ec909d335d7b69fd1db741e13.svg)](https://app.codesee.io/r/reviews?pr=938&src=https%3A%2F%2Fgithub.com%2Fgraphql-markdown%2Fgraphql-markdown)

#### Legend
<img src="https://app.codesee.io/Legend_v3.png" alt="CodeSee Map legend" width="220" height="156">
</details>
coveralls: 
[![Coverage Status](https://coveralls.io/builds/61929939/badge)](https://coveralls.io/builds/61929939)

coverage: 93.951% (-0.4%) from 94.362% when pulling **91e68647c162241ec909d335d7b69fd1db741e13 on chore/refactor-printer-legacy** into **6b6158cd7ec6369ed557e26d46d99f70bd28c4d3 on main**.


Patch:
diff --git a/packages/printer-legacy/src/group.ts b/packages/printer-legacy/src/group.ts
index cd5632de..0e9988ff 100644
--- a/packages/printer-legacy/src/group.ts
+++ b/packages/printer-legacy/src/group.ts
@@ -3,7 +3,6 @@ import type {
   SchemaEntity,
   GraphQLType,
   Maybe,
-  GraphQLNamedType,
 } from "@graphql-markdown/types";
 
 import { toSlug, getNamedType } from "@graphql-markdown/utils";
@@ -17,15 +16,20 @@ export const getGroup = (
     return "";
   }
 
-  const graphLQLNamedType: GraphQLNamedType = getNamedType(
-    type as Maybe<GraphQLType>,
-  )!;
-  const typeName = graphLQLNamedType.name || graphLQLNamedType.toString();
+  const graphQLNamedType = getNamedType(type as Maybe<GraphQLType>);
 
-  return typeCategory in groups &&
-    groups[typeCategory] &&
-    typeName in groups[typeCategory]! &&
-    groups[typeCategory]![typeName]
-    ? toSlug(groups[typeCategory]![typeName]!)
-    : "";
+  if (!graphQLNamedType) {
+    return "";
+  }
+
+  const typeName =
+    "name" in graphQLNamedType
+      ? graphQLNamedType.name
+      : String(graphQLNamedType);
+
+  if (!(typeCategory in groups && typeName in groups[typeCategory]!)) {
+    return "";
+  }
+
+  return toSlug(groups[typeCategory]![typeName]);
 };
diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts
index da8a133c..b773f225 100644
--- a/packages/utils/src/string.ts
+++ b/packages/utils/src/string.ts
@@ -6,10 +6,14 @@ import type { Maybe } from "@graphql-markdown/types";
 import { getObjPath } from "./object";
 
 export function stringCaseBuilder(
-  str: string,
+  str: Maybe<string>,
   transformation?: Maybe<(word: string) => string>,
   separator?: string,
 ): string {
+  if (typeof str !== "string") {
+    return "";
+  }
+
   const hasTransformation = typeof transformation === "function";
   const stringCase = replaceDiacritics(str)
     .replace(/([a-z]+|\d+)([A-Z])/g, "$1 $2")
@@ -21,7 +25,11 @@ export function stringCaseBuilder(
   return prune(stringCase, separator);
 }
 
-export function prune(str: string, char: string = ""): string {
+export function prune(str: Maybe<string>, char: string = ""): string {
+  if (typeof str !== "string") {
+    return "";
+  }
+
   let res = str;
 
   if (res.startsWith(char)) {
@@ -37,31 +45,43 @@ export function prune(str: string, char: string = ""): string {
   return res;
 }
 
-export function toSlug(str: string): string {
+export function toSlug(str: Maybe<string>): string {
+  if (typeof str !== "string") {
+    return "";
+  }
   return kebabCase(str);
 }
 
-export function toHTMLUnicode(char: string): string {
+export function toHTMLUnicode(char: Maybe<string>): string {
+  if (typeof char !== "string") {
+    return "";
+  }
   const unicodeChar = char.charCodeAt(0).toString(16).padStart(4, "0");
   return `&#x${unicodeChar.toUpperCase()};`;
 }
 
 export function escapeMDX(str: unknown): string {
-  return `${str}`.replace(/[<>{}]/g, toHTMLUnicode);
+  return `${String(str)}`.replace(/[<>{}]/g, toHTMLUnicode);
 }
 
-export function firstUppercase(word: string): string {
+export function firstUppercase(word: Maybe<string>): string {
+  if (typeof word !== "string") {
+    return "";
+  }
   const sliceUppercase = word.slice(0, 1).toUpperCase();
   const sliceDefaultCase = word.slice(1);
   return `${sliceUppercase}${sliceDefaultCase}`;
 }
 
-export function capitalize(word: string): string {
+export function capitalize(word: Maybe<string>): string {
+  if (typeof word !== "string") {
+    return "";
+  }
   return firstUppercase(word.toLowerCase());
 }
 
 // from https://stackoverflow.com/a/37511463
-export function replaceDiacritics(str: string): string {
+export function replaceDiacritics(str: Maybe<string>): string {
   if (typeof str !== "string") {
     return "";
   }
@@ -71,11 +91,17 @@ export function replaceDiacritics(str: string): string {
     .replace(/[\u0300-\u036f]/g, "");
 }
 
-export function startCase(str: string): string {
+export function startCase(str: Maybe<string>): string {
+  if (typeof str !== "string") {
+    return "";
+  }
   return stringCaseBuilder(str, firstUppercase, " ");
 }
 
-export function kebabCase(str: string): string {
+export function kebabCase(str: Maybe<string>): string {
+  if (typeof str !== "string") {
+    return "";
+  }
   return stringCaseBuilder(str, (word: string) => word.toLowerCase(), "-");
 }
 


Test patch:
diff --git a/packages/printer-legacy/tests/unit/group.test.ts b/packages/printer-legacy/tests/unit/group.test.ts
index 63f1fc87..18e697f4 100644
--- a/packages/printer-legacy/tests/unit/group.test.ts
+++ b/packages/printer-legacy/tests/unit/group.test.ts
@@ -2,7 +2,7 @@ import type { GraphQLNamedType } from "graphql/type/definition";
 
 jest.mock("@graphql-markdown/utils", () => {
   return {
-    toSlug: jest.fn(),
+    ...jest.requireActual("@graphql-markdown/utils"),
     getNamedType: jest.fn(),
   };
 });
@@ -12,39 +12,61 @@ import * as Utils from "@graphql-markdown/utils";
 import { getGroup } from "../../src/group";
 
 describe("group", () => {
-  beforeEach(() => {
-    jest.spyOn(Utils, "toSlug").mockImplementationOnce((name) => name);
-  });
-
   afterEach(() => {
     jest.restoreAllMocks();
   });
 
   describe("getGroup()", () => {
     test("returns empty string if groups not defined", () => {
-      const group = getGroup({ name: "FooBaz" }, undefined, undefined);
+      const group = getGroup({ name: "FooBaz" }, undefined, "objects");
 
       expect(group).toBe("");
     });
 
-    test("returns group name string if type has group", () => {
-      jest
-        .spyOn(Utils, "getNamedType")
-        .mockReturnValue("FooBaz" as unknown as GraphQLNamedType);
+    test.each([[undefined], [null]])(
+      "returns empty string if type is %s",
+      (value) => {
+        const group = getGroup(
+          value,
+          { objects: { FooBaz: "Group Test" } },
+          "objects",
+        );
 
+        expect(group).toBe("");
+      },
+    );
+
+    test("returns empty string if typeCategory not defined", () => {
       const group = getGroup(
         { name: "FooBaz" },
         { objects: { FooBaz: "Group Test" } },
-        "objects",
+        undefined,
       );
 
-      expect(group).toBe("Group Test");
+      expect(group).toBe("");
     });
 
+    test.each([[{ name: "FooBaz" }], [{ toString: (): string => "FooBaz" }]])(
+      "returns group name string if type has group",
+      (type) => {
+        jest
+          .spyOn(Utils, "getNamedType")
+          .mockReturnValue({ name: "FooBaz" } as unknown as GraphQLNamedType);
+
+        const group = getGroup(
+          type,
+          { objects: { FooBaz: "Group Test" } },
+          "objects",
+        );
+
+        expect(group).toBe("group-test");
+      },
+    );
+
     test("returns empty string if type not in group", () => {
       jest
         .spyOn(Utils, "getNamedType")
-        .mockReturnValue("FooBar" as unknown as GraphQLNamedType);
+        .mockReturnValue({ name: "FooBar" } as unknown as GraphQLNamedType);
 
       const group = getGroup(
         { name: "FooBar" },
@@ -55,4 +77,16 @@ describe("group", () => {
       expect(group).toBe("");
     });
   });
+
+  test("returns empty string if resolved named type is undefined", () => {
+    jest.spyOn(Utils, "getNamedType").mockReturnValue(undefined);
+
+    const group = getGroup(
+      { name: "FooBar" },
+      { objects: { FooBaz: "Group Test" } },
+      "objects",
+    );
+
+    expect(group).toBe("");
+  });
 });
diff --git a/packages/printer-legacy/tests/unit/relation.test.ts b/packages/printer-legacy/tests/unit/relation.test.ts
index 009a1c4c..2694aa86 100644
--- a/packages/printer-legacy/tests/unit/relation.test.ts
+++ b/packages/printer-legacy/tests/unit/relation.test.ts
@@ -1,4 +1,5 @@
-import { GraphQLScalarType, type GraphQLSchema } from "graphql";
+import type { GraphQLSchema } from "graphql";
+import { GraphQLScalarType } from "graphql";
 
 jest.mock("@graphql-markdown/utils", () => {
   return {
@@ -9,13 +10,13 @@ jest.mock("@graphql-markdown/utils", () => {
     isInputType: jest.fn(),
     isInterfaceType: jest.fn(),
     isObjectType: jest.fn(),
-    isOperation: jest.fn(),
+    isOperation: jest.fn(() => false),
     isScalarType: jest.fn(),
     isUnionType: jest.fn(),
     escapeMDX: jest.fn((s) => s),
     toSlug: jest.fn(),
     pathUrl: jest.fn(),
-    isNamedType: jest.fn(),
+    isNamedType: jest.fn(() => true),
     hasDirective: jest.fn(),
   };
 });
@@ -71,6 +72,8 @@ describe("relation", () => {
     test("returns empty string if type is undefined", () => {
       expect.hasAssertions();
 
+      jest.spyOn(Utils, "isNamedType").mockReturnValueOnce(false);
+
       const relation = printRelationOf(
         undefined,
         "RelationOf",
@@ -89,7 +92,7 @@ describe("relation", () => {
         description: "Lorem Ipsum",
       });
 
-      jest.spyOn(Utils, "isOperation").mockImplementation(() => true);
+      jest.spyOn(Utils, "isOperation").mockReturnValueOnce(true);
 
       const relation = printRelationOf(
         type,
@@ -104,7 +107,7 @@ describe("relation", () => {
     test("returns empty string if getRelation is not a function", () => {
       expect.hasAssertions();
 
-      const type = new GraphQLScalarType({
+      const type = new GraphQLScalarType<string, string>({
         name: "String",
         description: "Lorem Ipsum",
       });
@@ -119,6 +122,22 @@ describe("relation", () => {
       expect(relation).toBe("");
     });
 
+    test("returns empty string if schema is not defined", () => {
+      expect.hasAssertions();
+
+      const type = new GraphQLScalarType({
+        name: "String",
+        description: "Lorem Ipsum",
+      });
+
+      const relation = printRelationOf(type, "RelationOf", jest.fn(), {
+        ...DEFAULT_OPTIONS,
+        schema: undefined,
+      });
+
+      expect(relation).toBe("");
+    });
+
     test("returns empty string if getRelation returns undefined", () => {
       expect.hasAssertions();
 
@@ -127,6 +146,8 @@ describe("relation", () => {
         description: "Lorem Ipsum",
       });
 
+      jest.spyOn(Utils, "isNamedType").mockReturnValueOnce(true);
+
       const relation = printRelationOf(
         type,
         "RelationOf",
@@ -137,7 +158,7 @@ describe("relation", () => {
       expect(relation).toBe("");
     });
 
-    test("returns empty string if getRelation returns empty list", () => {
+    test("returns empty string if getRelation returns empty map", () => {
       expect.hasAssertions();
 
       const type = new GraphQLScalarType({
@@ -148,7 +169,7 @@ describe("relation", () => {
       const relation = printRelationOf(
         type,
         "RelationOf",
-        jest.fn().mockReturnValue([]),
+        jest.fn().mockReturnValue({ objects: [] }),
         DEFAULT_OPTIONS,
       );
 
diff --git a/packages/utils/tests/unit/string.test.ts b/packages/utils/tests/unit/string.test.ts
index 63575d3a..8da7bcff 100644
--- a/packages/utils/tests/unit/string.test.ts
+++ b/packages/utils/tests/unit/string.test.ts
@@ -18,6 +18,10 @@ describe("string", () => {
 
       expect(stringCaseBuilder(text, undefined, " ")).toBe(expected);
     });
+
+    test("returns empty string if param not a string", () => {
+      expect(stringCaseBuilder(undefined, undefined, " ")).toBe("");
+    });
   });
 
   describe("replaceDiacritics()", () => {
@@ -33,7 +37,7 @@ describe("string", () => {
     test("returns empty string if param not a string", () => {
       expect.hasAssertions();
 
-      expect(replaceDiacritics(undefined as unknown as string)).toBe("");
+      expect(replaceDiacritics(undefined)).toBe("");
     });
   });
 
@@ -60,6 +64,12 @@ describe("string", () => {
 
       expect(prune(text, "-")).toBe(expected);
     });
+
+    test("returns empty string if param not a string", () => {
+      expect.hasAssertions();
+
+      expect(prune(undefined)).toBe("");
+    });
   });
 
   describe("toSlug()", () => {
@@ -71,6 +81,12 @@ describe("string", () => {
 
       expect(toSlug(text)).toBe(expected);
     });
+
+    test("returns empty string if param not a string", () => {
+      expect.hasAssertions();
+
+      expect(toSlug(undefined)).toBe("");
+    });
   });
 
   describe("escapeMDX()", () => {
@@ -108,6 +124,12 @@ describe("string", () => {
         expect(capitalize(input)).toBe(expected);
       },
     );
+
+    test("returns empty string if param not a string", () => {
+      expect.hasAssertions();
+
+      expect(capitalize(undefined)).toBe("");
+    });
   });
 
   describe("interpolate()", () => {