prisma-binding icon indicating copy to clipboard operation
prisma-binding copied to clipboard

Error: Cannot read property 'type' of undefined, when generating Typescript for array fields

Open tomitrescak opened this issue 5 years ago • 37 comments

Hi, I am having issues generating Typescript schemas for queries returning arrays. Following works with no issues:

type Query {
  notifications(start: Int, end: Int): Notification
}

following throws an error below:

type Query {
  notifications(start: Int, end: Int): [Notification]!
}

Error:

TypeError: Cannot read property 'type' of undefined
    at getWhere (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/dist/utils.js:44:9)
    at /Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/utils.ts:41:12
    at Array.map (<anonymous>)
    at getTypesAndWhere (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/utils.ts:38:21)
    at Object.getExistsTypes (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/utils.ts:9:17)
    at PrismaTypescriptGenerator.renderExists (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/PrismaTypescriptGenerator.ts:71:20)
    at PrismaTypescriptGenerator.render (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/PrismaTypescriptGenerator.ts:19:32)
    at /Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/bin.ts:69:34
    at step (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/dist/bin.js:33:23)
    at Object.next (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/dist/bin.js:14:53)

tomitrescak avatar Jul 11 '18 12:07 tomitrescak

What are the exact steps you follow? How can I reproduce this?

marktani avatar Jul 11 '18 12:07 marktani

Hi.

Prisma.yml

endpoint: http://localhost:4466
datamodel: 
  - users.graphql
hooks:
  post-deploy:
    - graphql get-schema -p prisma
    - graphql codegen

.graphqlconfig

projects:
  app:
    schemaPath: "src/data/yoga/schema.graphql"
    includes: ["src/**/*.graphql"]
    extensions:
      endpoints:
        default: "http://localhost:4000"
      codegen:
        - generator: prisma-binding
          language: typescript
          output: 
            binding: src/data/generated/api.ts
  prisma:
    schemaPath: "src/data/generated/prisma.graphql"
    includes: ["src/**/*.graphql"]
    extensions:
      prisma: src/data/prisma/prisma.yml
      codegen:
        - generator: prisma-binding
          language: typescript
          output: 
            binding: src/data/generated/prisma.ts

users.graphql

type User {
  id: ID! @unique
  name: String!
  roles: [String!]!
}

Now, as for YOGA

schema.graphql

#import User from '../generated/prisma.graphql'

type Query {
  users: [User]!
}

Now I run prisma deploy

Running graphql get-schema -p prisma ✔
TypeError: Cannot read property 'type' of undefined
    at getWhere (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/dist/utils.js:44:9)
    at /Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/utils.ts:41:12
    at Array.map (<anonymous>)
    at getTypesAndWhere (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/utils.ts:38:21)
    at Object.getExistsTypes (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/utils.ts:9:17)
    at PrismaTypescriptGenerator.renderExists (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/PrismaTypescriptGenerator.ts:71:20)
    at PrismaTypescriptGenerator.render (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/PrismaTypescriptGenerator.ts:19:32)
    at /Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/src/bin.ts:69:34
    at step (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/dist/bin.js:33:23)
    at Object.next (/Users/tomi/Github/apps/corporatorts/node_modules/prisma-binding/dist/bin.js:14:53)


Running graphql codegen ✖

tomitrescak avatar Jul 11 '18 13:07 tomitrescak

Please share the following:

  • prisma version
  • Prisma server version
  • graphql --version
  • Node version
  • OS

marktani avatar Jul 11 '18 13:07 marktani

  • prisma version: prisma/1.10.2 (darwin-x64) node-v10.2.1
  • Prisma server version: ??? ... I use the docker image that was built yesterday (prismagraphql/prisma:1.11)
  • graphql --version: 2.16.4
  • Node version: 10.2.1
  • OS: Mac High Sierra

tomitrescak avatar Jul 11 '18 23:07 tomitrescak

[EDIT] I have just update Prisma to 1.11.1 and the error remains.

tomitrescak avatar Jul 12 '18 00:07 tomitrescak

Having the exact same problem. My setup:

  • prisma version: prisma/1.12.0 (linux-x64) node-v10.5.0
  • Prisma server version: prismagraphql/prisma:1.12
  • graphql --version: 2.16.4
  • Node version: 10.5.0
  • OS: Ubuntu 16.04

edorivai avatar Jul 22 '18 08:07 edorivai

I think I might understand what's happening here. I was using the prisma-binding against my app, as well as against the prisma database. It works against prisma, but fails against my own app. For my own app, I'm now using the graphql-binding, you can check https://github.com/graphql-binding/graphql-binding-github for an example.

I'm new to prisma and this was not 100% clear from the docs. I think most people that are just getting started will have the same setup as I do; a prisma service and an "app". Perhaps the codegen docs could more explicitly touch on this case, and outline that the prisma service needs prisma-binding, while your app needs the graphql-binding.

edorivai avatar Jul 22 '18 14:07 edorivai

@edorivai Your solution appears to work, but takes me back to the initial issue I had using other techniques. I need to support graphql imports.

# import TeamConnection from "./generated/prisma.graphql"
Error: Type "TeamConnection" not found in document.

mattferrin avatar Jul 24 '18 17:07 mattferrin

@edorivai Nevermind. Works perfectly using graphql-import. I'm just a little slow this time.

src/schema.js

// for typescript types generation only
const { makeExecutableSchema } = require("graphql-tools");
const { importSchema } = require("graphql-import");

const typeDefs = importSchema(__dirname + "/schema.graphql");
const schema = makeExecutableSchema({
  resolverValidationOptions: {
    requireResolversForResolveType: false
  },
  typeDefs: typeDefs
});

module.exports = schema;

.graphqlconfig.yml

projects:
  app:
    schemaPath: src/schema.graphql
    extensions:
      endpoints:
        default: http://localhost:4000
      codegen:
        - generator: graphql-binding
          language: typescript
          input:
            schema: src/schema.js
          output:
            binding: src/generated/app.ts
  database:
    schemaPath: src/generated/prisma.graphql
    extensions:
      prisma: database/prisma.yml
      codegen:
        - generator: prisma-binding
          language: typescript
          output:
            binding: src/generated/prisma.ts

mattferrin avatar Jul 24 '18 18:07 mattferrin

@mattferrin how do you obtain src/schema.js ?

tomitrescak avatar Jul 24 '18 19:07 tomitrescak

@tomitrescak You can create a new file name schema.js in your src directory. The copy and paste the top js code snippet into it. Should work. Let me know if you have issues.

mattferrin avatar Jul 24 '18 19:07 mattferrin

Yeah .. I just found out studying graphql-binding ;) Thanks for a SUPER fast answer!!!

tomitrescak avatar Jul 24 '18 19:07 tomitrescak

@tomitrescak And it looks like I'm going to use my schema.js file with graphql-code-generator instead too. Since my goal is to add static type checking to my server-side work and I can't take the Query, Mutation, and Subscription types generated and use them directly, graphql-code-generator seems like it will generate more fine-grained types for me. (And I've had success with it client-side anyway.)

{

  "scripts": {
    "build-graphql":
      "gql-gen --schema src/schema.js --template graphql-codegen-typescript-template --out ./src/generated/"
    "start":
      "npm run build-graphql && npm run tslint && dotenv -- nodemon -e ts,graphql -x ts-node src/index.ts",
  },
  "devDependencies": {
    "graphql-code-generator": "^0.10.3",
    "graphql-codegen-typescript-template": "^0.10.3"
  }
}

mattferrin avatar Jul 24 '18 20:07 mattferrin

@tomitrescak Looks like I was wrong. Neither generator works well to type my server-side. Not sure what the best approach is, but I'll work with what I got :)

mattferrin avatar Jul 24 '18 20:07 mattferrin

@mattferin I like the generated content by GraphQL-binding. With a little typescript magic it gives me effortless completely type safe resolvers.

Here is magic

import * as Types from '../../prisma';
export * from '../../generated/prisma';

import { GraphQLResolveInfo } from 'graphql';

import { Mutation as PrismaMutation, Query as PrismaQuery } from '../../generated/api';
import { LanguageCode, Prisma, User } from '../../generated/prisma';

export type FirstArgument<T> = T extends (arg1: infer U, ...args: any[]) => any ? U : any;

export type Remapped<T> = {
  [P in keyof T]: (
    parent: null | undefined,
    args: FirstArgument<T[P]>,
    ctx: Context,
    info?: GraphQLResolveInfo
  ) => any
};

export type Query = Partial<Remapped<PrismaQuery>>;
export type Mutation = Partial<Remapped<PrismaMutation>>;


export interface Context {
  db: Prisma;
  request: any;
  session: {
    user: User;
    language: LanguageCode;
  };
}

export type Resolver<T> = {
  [U in keyof Partial<typeof Types>]: {
    [P in keyof Partial<T>]: (parent: T, args: any, ctx: Context, info: GraphQLResolveInfo) => any
  }
};

And this how it’s used, everything is type safe, arguments, context ...

import { getUserId, Mutation, Notification, Query, Resolver } from './utils';

export const query: Query = {
  notifications(_parent, { start, end }, ctx, info) {
    return ctx.db.query.notifications(
      { where: { owner: { id: getUserId(ctx) } }, skip: start, last: end },
      info
    );
  }
};

export const mutation: Mutation = {
  async notify(_parent, args, ctx, info) {
    const user = await ctx.db.mutation.updateUser(
      {
        where: { id: args.userId },
        data: {
          notifications: {
            create: {
              code: args.code,
              params: { set: args.params },
              processInstance: {
                connect: {
                  id: args.processInstanceId
                }
              },
              visible: true
            }
          }
        }
      },
      info
    );

    // return user.notifications[0];
    // const notification = await ctx.db.query.notifications({ where: {     }); // .user()
    return true;
  }
};

export const resolver: Resolver<Notification> = {
  Notification: {
    text: async (parent, _args, ctx, info) => {
      const results = await ctx.db.query.localisations({
        where: { code: parent.code, language: ctx.session.language }
      });
      return results[0];
    }
  }
};

tomitrescak avatar Jul 25 '18 08:07 tomitrescak

@tomitrescak i don't understand some things in your strtategy :

  1. Differenciation API / Prisma(db)
import { Mutation as PrismaMutation, Query as PrismaQuery } from '../../generated/api';

would not it make more sense as :

import { Mutation as ApiMutation, Query as ApiQuery } from '../../generated/api';

since you are using those types for resolvers args and resolvers functions, while prisma function ytpes are already provided through Context.

So yea sound to me obvious both types matches since they are both from the generated prisma.ts, which isnot the point ?

  1. For example : api.schema.gql :
type Query {
    order(id: ID!): Product
   ...
}

... and then i would find after generation that generated/api.ts exposes :

export interface Query {
    order: <T = Order | null>(args: { where: OrderWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
   ...
}

which is exactly the same as in generated/prisma.ts huh ? should not it reflect the api schema instead of prisma schema ?
From there, obviously any types based on generated/api.ts will match types from Context since they are the same ....

Unless i'm missing something ?

Sharlaan avatar Jul 25 '18 10:07 Sharlaan

@tomitrescak Are the Types exported from the prisma package? My npm installed dependency has a blank type definitions file. Starting to clone prisma, but it's a bit of work to figure out.

mattferrin avatar Jul 25 '18 16:07 mattferrin

Guys, I’ll post full instructions tonight. Started to write them this morning but then my little one took over the day. Don’t waste time with the code above, there are a lot of assumptions there) I’ll post back in about 3-4 hours.

tomitrescak avatar Jul 25 '18 16:07 tomitrescak

You're awesome. It looks pretty sweet. Thanks.

mattferrin avatar Jul 25 '18 16:07 mattferrin

I haven't figured out how I broke the database playground yet, but the below was sufficient to get sweet type checking:

import { GraphQLResolveInfo } from "graphql";
import * as jwt from "jsonwebtoken";
import {
  Mutation as AppMutation,
  Query as AppQuery,
  Subscription as AppSubscription
} from "./generated/app";
import { Prisma } from "./generated/prisma";

type FirstArgument<T> = T extends (arg1: infer U, ...args: any[]) => any
  ? U
  : any;

type Remapped<T> = {
  [P in keyof T]: (
    parent: null | undefined,
    args: FirstArgument<T[P]>,
    ctx: IContext,
    info?: GraphQLResolveInfo
  ) => any
};

type SubscriptionRemapped<T> = {
  [P in keyof T]: {
    subscribe: (
      parent: null | undefined,
      args: FirstArgument<T[P]>,
      ctx: IContext,
      info?: GraphQLResolveInfo
    ) => any;
  }
};

export type Query = Remapped<AppQuery>;
export type Mutation = Remapped<AppMutation>;
export type Subscription = SubscriptionRemapped<AppSubscription>;

export interface IContext {
  db: Prisma;
  request: any;
}

mattferrin avatar Jul 25 '18 17:07 mattferrin

@Sharlaan , let me start with saying that you are right. Naming it PrismaMutation was not the best of the choices. I was planning to write a blog post about it, but had no time, so let me just do a quick intro to what is happening here.

The Goal

The goal is to have fully type safe resolvers of queries, mutations and types. It may seem like a lot of work to accomplish, but ultimately it is just a copy paste of one file, adjusting the import paths. Ultimately this is how it will look:

// the types below do all the heavy lifting of making everything type safe
import { Mutation, Notification, Query, Resolver } from './utils';

export const query: Query = {
  // params and ctx are type safe, parent and info are 'any'
  notifications(_parent, params, ctx, info) {} 
};

export const mutation: Mutation = {
  notify(_parent, args, ctx, info) {}
};

export const resolver: Resolver<Notification> = {
  // we provide two version of the solution, where type names in resolver are NOT type safe, 
  // or with a bit of extra work we can make it type safe
  Notification: {
    // parent, ctx is type safe, args and info is any
    text: async (parent, _args, ctx, info) => {}
  }
};

The benefit is obvious, just by stating Muation or Query as a type, the parent, context and arguments are automatically type safe, no need to define types for arguments. When schema is regenerated, it will automatically catch all errors.

The Problem

Graphql-Binding generates pretty awesome typescript definitions. Let's look how it generates a resolver for query notifications

export interface Query {
    notifications: <T = Notification[]>(args: { start?: Int, end?: Int }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> 
}

We can immediately see, that the headers of Apollo resolvers and generated GraphQL resolvers do not match.

// Apollo
resolver(parent, args, context, info)
// Generated
resolver(args, info, options)

The Solution

If you loved typescript, after reading following, hearing Typescript will play the song "Who Let The Dogs Out! Who!? Who!?" in your head ... guaranteed. My solution is based on statically remapping the header parameters from generated resolver to match the Apollo resolver. For this, we will use the new infer keyword to infer the Type. Following line returns the type of the first parameter of function. I should reference here the site where I found this but I cannot remember ;(

export type FirstArgument<T> = T extends (arg1: infer U, ...args: any[]) => any ? U : any;

We are now ready to create new remapped Apollo resolver. The definition below specifies, that every member of the object of type T is a function with the defined header, notably moving the first parameter to second position. 🥇

export type Remapped<T> = {
  [P in keyof T]: (
    parent: null | undefined,
    args: FirstArgument<T[P]>,
    ctx: Context,
    info?: GraphQLResolveInfo
  ) => any
};

We are now ready to define the type safe version of resolvers of mutations and queries (type resolver will follow soon). We need to import the resolvers from API and we need to also import types from Prisma, to make it part of the context.

import { GraphQLResolveInfo } from 'graphql';

import { Mutation as ApiMutation, Query as ApiQuery } from './generated/api';
import { Prisma } from './generated/prisma';

export interface Context {
  db: Prisma;
  request: any;
}

export type FirstArgument<T> = T extends (arg1: infer U, ...args: any[]) => any ? U : any;

export type Remapped<T> = {
  [P in keyof T]: (
    parent: null | undefined,
    args: FirstArgument<T[P]>,
    ctx: Context,
    info?: GraphQLResolveInfo
  ) => any
};

// Following will make all your query and mutation resolvers type safe
export type Query = Partial<Remapped<ApiQuery>>;
export type Mutation = Partial<Remapped<ApiMutation>>;

Type Resolvers: Version A - Almost Type Safe

Type resolvers will become almost type safe using following generic definition. Note the this is the problem line. The problem is, that the name of the type is not watched and can be anything.

export type Resolver<T, U = any> = {
  [index: string]: { // this is a problem
    [P in keyof Partial<T>]: (parent: T, args: U, ctx: Context, info: GraphQLResolveInfo) => any
  }
};

Type Resolvers: Version B - Type Safe

Unfortunately to make this type safe we need to do a bit of extra work as Typescript's in keyof does not work with namespaces. Therefore we need to manually define types that we will use in our resolvers. If you think this extra work is not worth it, just stick to version A.

// file: types.ts
import * as Api from './generated/api';

// export one variable of each type
export const Notifications: Api.Notifications;
export const User: Api.User;
import * as Types from './types';

export type Resolver<T> = {
  [U in keyof Partial<typeof Types>]: {
    [P in keyof Partial<T>]: (parent: T, args: any, ctx: Context, info: GraphQLResolveInfo) => any
  }
};

Wrap Up

That's all folks! Pure profit. No need to manually define arguments, context, everything is sorted as easy as following:

import { Query } from './utils';

export const query: Query = {
   // hit cmd+space here and hear "who let the dogs out!"
}

Just for completeness, here is the copy-paste version of the solution

// file: utils.ts
import { GraphQLResolveInfo } from 'graphql';

import { Mutation as ApiMutation, Query as ApiQuery } from './generated/api';
import { Prisma } from './generated/prisma';
import * as Types from './types; // you can omit this

export type FirstArgument<T> = T extends (arg1: infer U, ...args: any[]) => any ? U : any;

export type Remapped<T> = {
  [P in keyof T]: (
    parent: null | undefined,
    args: FirstArgument<T[P]>,
    ctx: Context,
    info?: GraphQLResolveInfo
  ) => any
};

export interface Context {
  db: Prisma;
}

export type Query = Partial<Remapped<ApiQuery>>;
export type Mutation = Partial<Remapped<ApiMutation>>;
export type Resolver<T> = {
  [U in keyof Partial<typeof Types>]: { // or [index: string]: {
    [P in keyof Partial<T>]: (parent: T, args: any, ctx: Context, info: GraphQLResolveInfo) => any
  }
};

tomitrescak avatar Jul 25 '18 20:07 tomitrescak

Thanks @edorivai i totally missed this :

Perhaps the codegen docs could more explicitly touch on this case, and outline that the prisma service needs prisma-binding, while your app needs the graphql-binding.

Sharlaan avatar Jul 26 '18 16:07 Sharlaan

back to error ' Cannot read property 'type' of undefined" I have found https://github.com/prisma/prisma-binding/issues/192#issuecomment-401418309 So I have add 'fake param' where

If you modify your buggyTypes query to include some arguments, it will resolve your issue. For example:

type Query { buggyTypes(where: ID!): [BuggyType!]! }

vladka avatar Sep 10 '18 21:09 vladka

I cannot see it being resolved and I believe it might be the same issue. While running prisma generate I have been facing the error:

Cannot read property 'type' of undefined

Step by step I discovered that it is returned for type within only id field.

To work around this bug each type definition should have at least two fields.

Skitionek avatar Oct 17 '18 07:10 Skitionek

@tomitrescak

export type Remapped<T> = {
  [P in keyof T]: (
    parent: null | undefined,
    args: FirstArgument<T[P]>,
    ctx: Context,
    info?: GraphQLResolveInfo
  ) => any
};

Have you found a way to add types to the return value. Using any has not been pleasant for me.

mattferrin avatar Oct 17 '18 23:10 mattferrin

@mattferrin I would have no idea where to start as each resolver returns it's own thing.

tomitrescak avatar Oct 18 '18 21:10 tomitrescak

@tomitrescak I'm a little confused honestly, but I think the below has compiled without issues at times, but at other times has failed with ReturnType<T> being inferred as {} instead. Then I have to revert back to any despite only a few properties erring.

type ReturnTypeInfer<T> = T extends (...args: any[]) => any
  ? ReturnType<T> // doesn't seem to work reliably, typescript static checking seems unpredictable
  : any;

type Remapped<T> = {
  [P in keyof T]: (
    parent: null | undefined,
    args: FirstArgument<T[P]>,
    ctx: IContext,
    info?: GraphQLResolveInfo
  ) => ReturnTypeInfer<T[P]>
};

I appreciate the response back regardless. Thanks.

mattferrin avatar Oct 18 '18 22:10 mattferrin

@mattferrin looks nice, I do not seem to have problems with your approach. It is working rock solid. Not sure why it's failing for you ;(

tomitrescak avatar Oct 22 '18 22:10 tomitrescak

What are the exact steps you follow? How can I reproduce this?

I used extended schema and add some type without arg where

type Query {
  getMyTypes: [MyType!]!
}

And got this error.

I just add where condition and all OK

type Query {
  getMyTypes(
    where: MyCond
  ): [MyType!]!
}

Error became when i create server directly without declare resolvers (I use them like as proxy);

api = new Prisma({
  typeDefs: 'src/schema/generated/api.graphql',
  endpoint: 'http://localhost:4000',
  secret: 'mysecret123',
  debug: false,
});

UPD: It's actual for list selections, results like usersOnline: [User!]!

usersOnline(where: UserWhereInput): [User!]! Works fine.

Fi1osof avatar Nov 03 '18 09:11 Fi1osof

@edorivai

I think I might understand what's happening here. I was using the prisma-binding against my app, as well as against the prisma database. It works against prisma, but fails against my own app. For my own app, I'm now using the graphql-binding, you can check https://github.com/graphql-binding/graphql-binding-github for an example.

I'm new to prisma and this was not 100% clear from the docs. I think most people that are just getting started will have the same setup as I do; a prisma service and an "app". Perhaps the codegen docs could more explicitly touch on this case, and outline that the prisma service needs prisma-binding, while your app needs the graphql-binding.

I am new to Prisma, and this was the source of my problem. The docs are not clear on this, and I suspect it may confuse a lot of people. Thanks for clearing that up.

ZenSoftware avatar Mar 06 '19 02:03 ZenSoftware