Repository: vazco/uniforms (ts)

Issue:
Typescript mismatch error on rc-2
I'm trying to build a form using AutoForm from a bare bones schema, I think I have a very minimal example:

```
import { buildASTSchema, parse } from "graphql";
import React from "react";
import { AutoForm } from "uniforms-bootstrap4";
import { GraphQLBridge } from "uniforms-bridge-graphql";

type CustomerUpdateAutoFormProps = {};
export const CustomerUpdateAutoForm: React.SFC<CustomerUpdateAutoFormProps> = ({}) => {
  const schema = `    type Author {
    id:        String!
    firstName: String
    lastName:  String
}

type Post {
    id:     Int!
    author: Author!
    title:  String
    votes:  Int
}
`;
  const astSchema = buildASTSchema(parse(schema));
  const schemaType = astSchema.getType("Post");
  const schemaValidator = () => {};
  const bridge = new GraphQLBridge(schemaType!, schemaValidator);
  return <AutoForm schema={bridge} onSubmit={console.log} />;
};
```

But it results in the follow error:
```
TypeScript error in /home/pljns/Desktop/picket-frontend/src/components/administrator/customer/CustomerUpdateAutoForm.tsx(24,36):
Argument of type 'GraphQLNamedType' is not assignable to parameter of type 'GraphQLInputObjectType | GraphQLObjectType<any, any, { [key: string]: any; }>'.
  Type 'GraphQLScalarType' is not assignable to type 'GraphQLInputObjectType | GraphQLObjectType<any, any, { [key: string]: any; }>'.
    Type 'GraphQLScalarType' is missing the following properties from type 'GraphQLObjectType<any, any, { [key: string]: any; }>': isTypeOf, getFields, getInterfaces  TS2345

    22 |   const schemaType = astSchema.getType("Post");
    23 |   const schemaValidator = () => {};
  > 24 |   const bridge = new GraphQLBridge(schemaType!, schemaValidator);
       |                                    ^
    25 |   return <AutoForm schema={bridge} onSubmit={console.log} />;
    26 | };
    27 | 
```

It's because typescript seems to be expecting ` isTypeOf, getFields, getInterfaces`, why is that? Is there a version of Graphql that does work? I'm using 15.3.0.

My package.json file:

```
{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@apollo/client": "^3.0.1",
    "@mapbox/polyline": "^1.1.1",
    "@stripe/stripe-js": "^1.5.0",
    "@testing-library/jest-dom": "^5.11.1",
    "@testing-library/react": "^10.4.7",
    "@testing-library/user-event": "^12.0.11",
    "@types/jest": "^26.0.5",
    "@types/jquery": "^3.5.0",
    "@types/mapbox-gl": "^1.11.1",
    "@types/node": "^14.0.23",
    "@types/react": "^16.9.0",
    "@types/react-big-calendar": "^0.24.2",
    "@types/react-bootstrap": "^1.0.1",
    "@types/react-bootstrap-typeahead": "^3.4.6",
    "@types/react-datepicker": "^3.0.2",
    "@types/react-dom": "^16.9.0",
    "@types/react-map-gl": "^5.2.5",
    "@types/react-router-dom": "^5.1.3",
    "@types/yup": "^0.29.3",
    "babel-plugin-import": "^1.13.0",
    "bootstrap": "^4.4.1",
    "chart.js": "^2.9.3",
    "customize-cra": "^1.0.0",
    "date-fns": "^2.14.0",
    "dotenv": "^8.2.0",
    "formik": "^2.1.4",
    "graphql": "^15.3.0",
    "mapbox-gl": "^1.11.1",
    "moment": "^2.26.0",
    "node-sass": "^4.13.1",
    "react": "^16.13.0",
    "react-big-calendar": "^0.26.0",
    "react-bootstrap": "^1.0.0-beta.17",
    "react-bootstrap-typeahead": "^5.1.0",
    "react-chartjs-2": "^2.9.0",
    "react-datepicker": "^3.1.3",
    "react-dom": "^16.13.0",
    "react-icons": "^3.9.0",
    "react-json-view": "^1.19.1",
    "react-map-gl": "^5.2.7",
    "react-phone-input-2": "^2.13.5",
    "react-router-dom": "^5.1.2",
    "react-scripts": "3.4.1",
    "typescript": "~3.9.7",
    "uniforms": "^3.0.0-rc.2",
    "uniforms-bootstrap4": "^3.0.0-rc.2",
    "uniforms-bridge-graphql": "^3.0.0-rc.2",
    "yup": "^0.29.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "build-fragment": "node src/schemaQuery.js",
    "generate": "get-graphql-schema http://localhost:4000/graphql > ./src/graphql/schema.gql && gqlg --schemaFilePath ./src/graphql/schema.gql --destDirPath ./src/graphql/queries/ --depthLimit=3 && graphql-codegen --config codegen.js"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@graphql-codegen/cli": "^1.12.2",
    "@graphql-codegen/typescript": "^1.12.2",
    "@graphql-codegen/typescript-operations": "^1.12.2",
    "@graphql-codegen/typescript-react-apollo": "^1.12.2",
    "get-graphql-schema": "^2.1.2",
    "gql-generator": "^1.0.12"
  }
}
```

Is this fixed by the latest commit? Is there a way I can specify this in my package.json file? When will the next release be?

I really like your idea and look forward to getting it working one way or another in the coming days, thank you for any help.

Pull request description:
Reworked GraphQLBridge.getField (closes #786).
As reported and discussed in #781, #784, and #786 , current `GraphQLBridge` is not well-typed. It got better with #784, but it was still _weird_. This change reviews this behavior, leading to a cleaner implementation and a clean `GraphQLType` as an input.

There is one breaking change, but I think no one was actually using that - the second argument of `getField` got removed. It means, that `field.type` prop will be `GraphQLNonNull<T>` instead of `T` for non-nullable types.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd5233ab..bb7b1b47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## Next:
+
+- **Breaking:** Reworked `GraphQLBridge.getField`. [\#769](https://github.com/vazco/uniforms/issues/769)
+
 ## [v3.0.0-rc.3](https://github.com/vazco/uniforms/tree/v3.0.0-rc.3) (2020-08-13)
 
 - **Breaking:** Minimum `graphql` version for `uniforms-bridge-graphql` is now `15.0.0`. [\#781](https://github.com/vazco/uniforms/issues/781)
diff --git a/packages/uniforms-bridge-graphql/src/GraphQLBridge.ts b/packages/uniforms-bridge-graphql/src/GraphQLBridge.ts
index a8b282bd..06b9258e 100644
--- a/packages/uniforms-bridge-graphql/src/GraphQLBridge.ts
+++ b/packages/uniforms-bridge-graphql/src/GraphQLBridge.ts
@@ -1,4 +1,13 @@
-import * as graphql from 'graphql/type/definition';
+import {
+  GraphQLInputField,
+  GraphQLType,
+  getNullableType,
+  isInputObjectType,
+  isListType,
+  isNonNullType,
+  isObjectType,
+  isScalarType,
+} from 'graphql/type/definition';
 import invariant from 'invariant';
 import lowerCase from 'lodash/lowerCase';
 import memoize from 'lodash/memoize';
@@ -9,28 +18,20 @@ function extractValue(x: boolean | null | string | undefined, y: string) {
   return x === false || x === null ? '' : x !== true && x !== undefined ? x : y;
 }
 
-// FIXME: What type is it? Maybe there's a helper in `graphql` for that?
-function extractFromNonNull(x: any) {
-  return x && graphql.isNonNullType(x.type) ? { ...x, type: x.type.ofType } : x;
-}
-
 function toHumanLabel(label: string) {
   return upperFirst(lowerCase(label));
 }
 
 export default class GraphQLBridge extends Bridge {
   constructor(
-    public schema: graphql.GraphQLInputObjectType | graphql.GraphQLObjectType,
+    public schema: GraphQLType,
     public validator: (model: Record<string, any>) => any,
     public extras: Record<string, any> = {},
   ) {
     super();
 
     // Memoize for performance and referential equality.
-    this.getField = memoize(
-      this.getField,
-      (name, returnExtracted = true) => `${name}:${returnExtracted}`,
-    );
+    this.getField = memoize(this.getField);
     this.getSubfields = memoize(this.getSubfields);
     this.getType = memoize(this.getType);
   }
@@ -66,53 +67,34 @@ export default class GraphQLBridge extends Bridge {
     return [];
   }
 
-  getField(name: string, returnExtracted = true) {
-    return joinName(null, name).reduce((definition, next, index, array) => {
-      if (next === '$' || next === '' + parseInt(next, 10)) {
+  getField(name: string) {
+    const root = { name: '', type: this.schema } as GraphQLInputField;
+
+    return joinName(null, name).reduce((field, namePart) => {
+      const fieldType = getNullableType(field.type);
+
+      if (namePart === '$' || namePart === '' + parseInt(namePart, 10)) {
         invariant(
-          graphql.isListType(definition.type),
+          isListType(fieldType),
           'Field not found in schema: "%s"',
           name,
         );
-        definition = { type: extractFromNonNull(definition.type.ofType) };
-        // @ts-ignore: Not public API.
-      } else if (definition.type && definition.type._fields) {
-        // @ts-ignore: Not public API.
-        definition = definition.type._fields[next];
-      } else {
-        // @ts-ignore: Incorrect `definition` type.
-        definition = definition[next];
-      }
-
-      invariant(definition, 'Field not found in schema: "%s"', name);
 
-      const isLast = array.length - 1 === index;
-      if (isLast && !returnExtracted) {
-        return definition;
+        return { ...field, type: fieldType.ofType };
       }
 
-      const extracted = extractFromNonNull(definition);
-
-      if (
-        (isLast && returnExtracted) ||
-        !graphql.isObjectType(extracted.type)
-      ) {
-        return extracted;
+      if (isInputObjectType(fieldType) || isObjectType(fieldType)) {
+        const fields = fieldType.getFields();
+        invariant(namePart in fields, 'Field not found in schema: "%s"', name);
+        return fields[namePart] as GraphQLInputField;
       }
 
-      invariant(
-        extracted.type.getFields,
-        'Field not found in schema: "%s"',
-        name,
-      );
-
-      return extracted.type.getFields();
-    }, this.schema.getFields());
+      invariant(false, 'Field not found in schema: "%s"', name);
+    }, root);
   }
 
   getInitialValue(name: string, props: Record<string, any> = {}): any {
     const type = this.getType(name);
-
     if (type === Array) {
       const item = this.getInitialValue(joinName(name, '0'));
       const items = (props.initialCount as number) || 0;
@@ -124,80 +106,63 @@ export default class GraphQLBridge extends Bridge {
       return {};
     }
 
-    const defaultValue = this.getField(name).defaultValue;
-
-    return defaultValue === undefined
-      ? this.extras[name]?.initialValue
-      : defaultValue;
+    const { defaultValue } = this.getField(name);
+    return defaultValue ?? this.extras[name]?.initialValue;
   }
 
-  // eslint-disable-next-line complexity
   getProps(nameNormal: string, props: Record<string, any> = {}) {
     const nameGeneric = nameNormal.replace(/\.\d+/g, '.$');
 
-    const field = this.getField(nameGeneric, false);
-    const fieldType = extractFromNonNull(field).type;
-
-    const ready = {
-      required: graphql.isNonNullType(field.type),
+    const { name, type } = this.getField(nameGeneric);
+    const result = {
+      required: isNonNullType(type),
       ...this.extras[nameGeneric],
       ...this.extras[nameNormal],
     };
 
-    if (graphql.isScalarType(fieldType) && fieldType.name === 'Float') {
-      ready.decimal = true;
+    const fieldType = getNullableType(type);
+    if (isScalarType(fieldType) && fieldType.name === 'Float') {
+      result.decimal = true;
     }
 
-    // @ts-ignore: `field.name` is not a string?
-    ready.label = extractValue(ready.label, toHumanLabel(field.name));
+    result.label = extractValue(result.label, toHumanLabel(name));
 
-    const options = props.options || ready.options;
+    const options = props.options || result.options;
     if (options) {
       if (!Array.isArray(options)) {
-        ready.transform = (value: any) => options[value];
-        ready.allowedValues = Object.keys(options);
+        result.transform = (value: any) => options[value];
+        result.allowedValues = Object.keys(options);
       } else {
-        ready.transform = (value: any) =>
+        result.transform = (value: any) =>
           options.find(option => option.value === value).label;
-        ready.allowedValues = options.map(option => option.value);
+        result.allowedValues = options.map(option => option.value);
       }
     }
 
-    return ready;
+    return result;
   }
 
-  getSubfields(name?: string) {
-    if (!name) {
-      return Object.keys(this.schema.getFields());
-    }
-
-    const fieldType = this.getField(name).type;
-
-    if (
-      graphql.isObjectType(fieldType) ||
-      graphql.isInputObjectType(fieldType)
-    ) {
-      return Object.keys(fieldType.getFields());
-    }
-
-    return [];
+  getSubfields(name = '') {
+    const type = getNullableType(this.getField(name).type);
+    return isInputObjectType(type) || isObjectType(type)
+      ? Object.keys(type.getFields())
+      : [];
   }
 
   getType(name: string) {
-    const fieldType = this.getField(name).type;
-
-    if (graphql.isListType(fieldType)) return Array;
-    if (graphql.isObjectType(fieldType)) return Object;
-    if (graphql.isInputObjectType(fieldType)) return Object;
-    if (graphql.isScalarType(fieldType)) {
-      if (fieldType.name === 'ID') return String;
-      if (fieldType.name === 'Int') return Number;
-      if (fieldType.name === 'Float') return Number;
-      if (fieldType.name === 'String') return String;
-      if (fieldType.name === 'Boolean') return Boolean;
+    const type = getNullableType(this.getField(name).type);
+
+    if (isInputObjectType(type) || isObjectType(type)) return Object;
+    if (isListType(type)) return Array;
+    if (isScalarType(type)) {
+      if (type.name === 'Boolean') return Boolean;
+      if (type.name === 'Float') return Number;
+      if (type.name === 'ID') return String;
+      if (type.name === 'Int') return Number;
+      if (type.name === 'String') return String;
     }
 
-    return fieldType;
+    return type;
   }
 
   getValidator(/* options */) {
diff --git a/reproductions/schema/graphql-schema.tsx b/reproductions/schema/graphql-schema.tsx
index ffa2b7e9..fcfe804d 100644
--- a/reproductions/schema/graphql-schema.tsx
+++ b/reproductions/schema/graphql-schema.tsx
@@ -25,7 +25,6 @@ const args = {
   title: { label: 'Horse A', placeholder: 'Horse B' },
 };
 
-const type = buildASTSchema(parse(schema)).getType('Address');
-if (!(type instanceof GraphQLObjectType)) throw new Error('Invalid type.');
+const type = buildASTSchema(parse(schema)).getType('Address')!;
 
 export const bridge = new GraphQLBridge(type, validator, args);


Test patch:
diff --git a/packages/uniforms-bridge-graphql/__tests__/GraphQLBridge.ts b/packages/uniforms-bridge-graphql/__tests__/GraphQLBridge.ts
index baf46a1a..f36dcf7d 100644
--- a/packages/uniforms-bridge-graphql/__tests__/GraphQLBridge.ts
+++ b/packages/uniforms-bridge-graphql/__tests__/GraphQLBridge.ts
@@ -78,23 +78,19 @@ describe('GraphQLBridge', () => {
   const astT = buildASTSchema(parse(schemaT));
 
   const bridgeI = new GraphQLBridge(
-    astI.getType('Post') as GraphQLInputObjectType,
+    astI.getType('Post')!,
     schemaValidator,
     schemaData,
   );
   const bridgeT = new GraphQLBridge(
-    astT.getType('Post') as GraphQLObjectType,
+    astT.getType('Post')!,
     schemaValidator,
     schemaData,
   );
 
   describe('#constructor()', () => {
     it('always ensures `extras`', () => {
-      const bridge = new GraphQLBridge(
-        astI.getType('Post') as GraphQLInputObjectType,
-        schemaValidator,
-      );
-
+      const bridge = new GraphQLBridge(astI.getType('Post')!, schemaValidator);
       expect(bridge.extras).toEqual({});
     });
   });
@@ -197,10 +193,10 @@ describe('GraphQLBridge', () => {
     });
 
     it('throws on not found field', () => {
-      expect(() => bridgeI.getField('x')).toThrow(/Field not found in schema/);
-      expect(() => bridgeI.getField('author.x')).toThrow(
-        /Field not found in schema/,
-      );
+      const error = /Field not found in schema/;
+      expect(() => bridgeI.getField('x')).toThrow(error);
+      expect(() => bridgeI.getField('author.x')).toThrow(error);
+      expect(() => bridgeI.getField('author.tags.x')).toThrow(error);
     });
   });