Nexus-plugin-prisma: Expose all fields with nexus-prisma v2

Created on 24 Jun 2019  路  21Comments  路  Source: graphql-nexus/nexus-plugin-prisma

Description

For security reasons, we removed the ability to expose all fields from a model by using t.prismaFields(['*']).

However, we still think that there might be a lot of advantages to enabling that when getting started.

Being able to very quickly expose a working graphql server is what made the success of graphcool, and nexus-prisma somehow brought back that convenience.

However, we want to make things a lot more explicit that it might not be safe at all to do so.

Proposal

Through some prototype: true flag defined in the generator, we could enable a method __all() on t.crud and t.model, so that you can do:

generator nexus_prisma {
  provider = "nexus-prisma"
  prototype = true
}

Which would enable:

export const User = objectType({
  name: 'User',
  definition(t) {
    t.model.__all() // Equal to pick all
  }
})

// With pick
export const User = objectType({
  name: 'User',
  definition(t) {
    t.model.__all({ pick: ['someField', 'otherField'] })
  }
})

// With omit
export const User = objectType({
  name: 'User',
  definition(t) {
    t.model.__all({ omit: ['someField', 'otherField'] })
  }
})
scopprojecting typfeat

Most helpful comment

I can see the reasoning behind wanting to remove the ability to expose all fields for security purposes but because you would have to explicitly declare you want to add all fields I don't see the need for having an additional flag especially since removing it would then make the code useless/break.

I agree with @Hebilicious that there are legit use cases outside of prototyping and I don't see any additional security added by not allowing this as default, whoever decides to use the function has explicitly chosen to do so, you can't accidentally expose everything unless you indent to. Of course its possible for someone to screw up and forget they've done it but that is not something nexus should be concerned with especially if it impedes legit use cases.

In the majority of my current use cases I prefer to define my schema by black-listing rather than white-listing, I would only need to remove 1 or 2 fields from each model at most or following a common pattern, with only an option for white-listing I end up basically duplicating the model and having to change things in more than one place often during development. Maybe there is a nice way to allow both black-list or white-list methodologies here?

I played around tonight to see how I could use the current implantation to fit my needs and I didn't have too much problem working around it thanks to what information was already available to me via proton, I am not sure this is directly related to this issue but maybe this will spark some ideas.

//
const photon = new Photon()
const { queryType, mutationType, modelMap } = photon.dmmf;

//
const Query = objectType({
  name: 'Query',
  definition(t) {
    t.crud.findOneUser();
    forEach((f) => {
      if (typeof t.crud[f.name] === 'function') {
        // additional filters and abstraction here
        t.crud[f.name]();
      }
    }, queryType.fields);
  },
})

//
const Mutation = objectType({
  name: 'Mutation',
  definition(t) {
    forEach((f) => {
      if (typeof t.crud[f.name] === 'function') {
        // additional filters and abstraction here
        t.crud[f.name]();
      }
    }, mutationType.fields);
  },
});

//
const types = map(({ name, fields }) => objectType({
  name,
  definition(t) {
    forEach(f => {
      // additional filters and abstraction here
      t.model[f.name]();
    }, fields);
  }
}), modelMap);

//
const BatchPayload = objectType({
  name: "BatchPayload",
  definition(t) {
    t.field("count", { type: "Int", })
  },
});

//
const schema = makeSchema({
  types: [Query, Mutation, ...types, BatchPayload, nexusPrisma],
  outputs: {
    schema: join(__dirname, '/generated/schema.graphql'),
  },
  typegenAutoConfig: {
    sources: [
      {
        source: '@generated/photon',
        alias: 'photon',
      },
    ],
  },
})

This effectively exposes everything but allows me to filter out what I don't want, for example I would rather have an external config that will effect what will be exposed to reduce boilerplate and make things generally more reusable.

All 21 comments

I believe this is quite an important feature.
One of my favorite use case for nexus-prisma is the ability to do this to quickly 'augment' a type:

export const customModel = prismaObjectType({
    name: "User",
    definition: t => {
        t.prismaFields(["*"])
        t.string("fullName", { resolve: ({ firstName, lastName }, args, ctx, info) => `${firstName} ${lastName}` })
    }
})

I understand the concern of '*' being dangerous. However there's some legit use cases for it outside of prototyping, depending of the type of application you're building.
I might be arguing semantics here, but could the naming be enableAll = true to reflect that it should be used only if you understand the implications, but is still valid outside of prototyping?

I can see the reasoning behind wanting to remove the ability to expose all fields for security purposes but because you would have to explicitly declare you want to add all fields I don't see the need for having an additional flag especially since removing it would then make the code useless/break.

I agree with @Hebilicious that there are legit use cases outside of prototyping and I don't see any additional security added by not allowing this as default, whoever decides to use the function has explicitly chosen to do so, you can't accidentally expose everything unless you indent to. Of course its possible for someone to screw up and forget they've done it but that is not something nexus should be concerned with especially if it impedes legit use cases.

In the majority of my current use cases I prefer to define my schema by black-listing rather than white-listing, I would only need to remove 1 or 2 fields from each model at most or following a common pattern, with only an option for white-listing I end up basically duplicating the model and having to change things in more than one place often during development. Maybe there is a nice way to allow both black-list or white-list methodologies here?

I played around tonight to see how I could use the current implantation to fit my needs and I didn't have too much problem working around it thanks to what information was already available to me via proton, I am not sure this is directly related to this issue but maybe this will spark some ideas.

//
const photon = new Photon()
const { queryType, mutationType, modelMap } = photon.dmmf;

//
const Query = objectType({
  name: 'Query',
  definition(t) {
    t.crud.findOneUser();
    forEach((f) => {
      if (typeof t.crud[f.name] === 'function') {
        // additional filters and abstraction here
        t.crud[f.name]();
      }
    }, queryType.fields);
  },
})

//
const Mutation = objectType({
  name: 'Mutation',
  definition(t) {
    forEach((f) => {
      if (typeof t.crud[f.name] === 'function') {
        // additional filters and abstraction here
        t.crud[f.name]();
      }
    }, mutationType.fields);
  },
});

//
const types = map(({ name, fields }) => objectType({
  name,
  definition(t) {
    forEach(f => {
      // additional filters and abstraction here
      t.model[f.name]();
    }, fields);
  }
}), modelMap);

//
const BatchPayload = objectType({
  name: "BatchPayload",
  definition(t) {
    t.field("count", { type: "Int", })
  },
});

//
const schema = makeSchema({
  types: [Query, Mutation, ...types, BatchPayload, nexusPrisma],
  outputs: {
    schema: join(__dirname, '/generated/schema.graphql'),
  },
  typegenAutoConfig: {
    sources: [
      {
        source: '@generated/photon',
        alias: 'photon',
      },
    ],
  },
})

This effectively exposes everything but allows me to filter out what I don't want, for example I would rather have an external config that will effect what will be exposed to reduce boilerplate and make things generally more reusable.

Hi, I would like to see this feature as well.

For my use case I am importing financial data from a 3rd-party into Prisma and want to expose that information to a GraphQL server. The 3rd party in this case is IEX (example api). When data is imported from these API's, nodes are inter-linked using the @relation annotation and differentiated by mapping iexId to the id of the node. Overall this spans around 30 types with around 400 fields, with new types and fields being added regularly.

With Prisma v1 it was trivial to instantly create a GraphQL server that allows complex queries of all the data by using the generated prisma.graphql file.

For example, my schema looked like this.

# import IEXSymbolInput, IEXSymbol from "../generated/prisma.graphql"
type Query {
  symbols(where: IEXSymbolInput!): IEXSymbol!
}

And a query might look like this:

query {
  symbols({symbol: "AAPL"}) {
    symbol
    name
    marketData {
      company {
        CEO
        companyName
        exchange
      }
      quote {
        latestPrice
        latestVolume
      }
      balancesheet {
        currentCash
        totalLiabilities
      }
    }
  }
}

This fit my use case very well, since I defined my fields only one time in my Prisma model, which generated the GraphQL types, which I then feed into graphql-schema-typescript for client types.

With Prisma2 and Nexus, having to re-define all these fields and types is not really ideal, although the script @michaelmitchell posted looks like it could work for me.

@michaelmitchell I ran across this after having implemented my own solution independently, albeit yours seems a little nicer, since it works off the photon.dmmf, which I didn't know about, but I don't know whether photon.dmmf could or will change its structure or even if it is meant to be accessed (prisma2 and related projects don't have well unified docs to explore all of this).

For anyone else looking I wrote the following:

export function exposeFields(model, blacklist = []) {
  Object.keys(model)
    .filter(field => !blacklist.includes(field))
    .forEach(field => model[field]());
}

const queryVerbage = ['findOne', 'findMany'];
export function exposeQueries(crud, blacklist = []) {
  const caseInsensitiveBlacklist = blacklist.map(_ => _.toLowerCase());

  Object.keys(crud)
    .filter(query => {
      const isTypeBlacklisted = queryVerbage.some(verbage => {
        const type = query.replace(verbage, '');
        return caseInsensitiveBlacklist.includes(type);
      });

      return !isTypeBlacklisted && !blacklist.includes(query);
    })
    .forEach(query => crud[query]());
}

const mutationVerbage = ['create', 'delete', 'update', 'upsert'];
export function exposeMutations(crud, blacklist = []) {
  const caseInsensitiveBlacklist = blacklist.map(_ => _.toLowerCase());

  Object.keys(crud)
    .filter(mutation => {
      const isTypeBlacklisted = mutationVerbage.some(verbage => {
        const type = mutation.replace(verbage, '');
        return caseInsensitiveBlacklist.includes(type);
      });

      return !isTypeBlacklisted && !blacklist.includes(mutation);
    })
    .forEach(mutation => crud[mutation]());
}

export const PrismaType = {
  Model: Symbol('field'),
  Query: Symbol('query'),
  Mutation: Symbol('mutation'),
}

function match(t, funcs) {
  return funcs[t]();
}

export function expose({ src, type, blacklist = [] }) {
  match(type, {
    [PrismaType.Model]: _ => exposeFields(src, blacklist),
    [PrismaType.Query]: _ => exposeQueries(src, blacklist),
    [PrismaType.Mutation]: _ => exposeMutations(src, blacklist),
  });
}

export function objType({ name, type, blacklist = [] }) {
  return objectType({
    name,
    definition(t) {
      expose({ 
        src: match(type, {
          [PrismaType.Model]: _ => t.model,
          [PrismaType.Query]: _ => t.crud,
          [PrismaType.Mutation]: _ => t.crud,
        }), 
        type, 
        blacklist 
      });
    },
  });
}

Essentially, nexus-prisma could and IMO should basically offer a 1 line function to open this all up. I'm basically writing it & you basically wrote it, which means there is definitely some form of demand.

@michaelmitchell what map and forEach method are you utilizing there

I also would love to see this feature re-added to Nexus _outside of prototyping_. As @michaelmitchell stated above, I too would prefer to use a blacklist approach. One of my models has 55 or so fields鈥攊t's a hassle to write all the t.model.<fieldName>() calls initially, and keeping the Nexus definition in line with the actual Prisma schema definition isn't fun as well.

@Weakky have you guys written anywhere what exactly the security issue was in t.prismaFields(['*'])? Is it absolutely out of the question for use in a production environment? What if you guys added some sort of keyword making sure the developer knows the potential risks, a la React's UNSAFE_componentWillReceiveProps()?

We use currently prisma1 and expose a grapql-api for an admin-crud-application (using https://github.com/marmelab/react-admin/ and https://github.com/Weakky/ra-data-opencrud/). We use prisma-binding to forward everything to prisma and add graphql-shield middleware to restrict access to queries and mutations.

With prisma2, nexus-prisma seems to be the way to go, but without t.prismaFields(['*']) it will introduce a lot of additional boilerplate. I would like the prisma-schema to be the source of truth (at least for non augmented types), but without t.prismaFields(['*']) there is a lot of redundancies.

I would just readd with, without any generator flag.

The repetition is something that is of concern with us as well. Imagine repeating this in a large application each time models change. This is a source of all sort of bugs and confusion not to mention the boilerplate.

I'm not sure if it's the exact same issue I'm having but it seems similar. I'm currently migrating my Prisma 1 (with nexus) app to Prisma 2 and in my Prisma 1 app, I could use something like this in my Query resolver:

t.prismaFields([
  "categories",
  "...",
  "..."
])

This would expose these to the API. Now, I believe, I have to do the following:

t.crud.categories()

If I do that, however, it gives me the following:

Missing type Category, did you forget to import a type to the root query?

So, to get around that, I have to manually create the objectType for Category in /src/types/, right? Is this not auto-generated? I hope I'm missing something here as I thought the whole point was that this could be generated from my schema. I have quite a big app with many models (some related, some with enums, etc.). Do I need to manually write these?

From a brief discussion in the Prisma Slack, it seems like this is a security measure to ensure that we're explicit in what we want to expose to the API. I get that, but having to manually write this boilerplate seems like a real downside to this approach. Hopefully, though, I'm completely missing something here as I'm new to this stuff.

Thanks for following up @darrylyoung. I was coming here to reference your slack thread https://prisma.slack.com/archives/CM2LEN7JL/p1574934803028200. But awesome to see you already came here to contribute. 馃檶

Hopefully, though, I'm completely missing something here as I'm new to this stuff.

You're not missing anything, about the current API. Take the fact that this issue is open and considered important by the team as a sign that things could change in the future. I don't think anyone is convinced (on the team) that the API has to be mutually exclusive between security and ease-of-use/velocity/productivity.

Thanks for the update, @jasonkuhrt. I'm happy to contribute wherever I can if it helps.

You're not missing anything, about the current API. Take the fact that this issue is open and considered important by the team as a sign that things could change in the future. I don't think anyone is convinced (on the team) that the API has to be mutually exclusive between security and ease-of-use/velocity/productivity.

I'm glad to hear that it's an important issue for the team. I'll keep an eye on things and see where API goes. Thanks for the feedback.

Really +1 for this.

I have multiple big app with multiple schema and wanted to migrate it to new cool nexus and prisma2, and we are impressed with prisma 2 and this is a blocker for us.

I thought I could expose everything myself using a small plugin like following,

But now prismaObjectType is removed from the plugin, prisma itself doesn't allow generating graphql yet. Prisma 1 had nice support for subscription, and all those nice enum and so on, it looks so cool!

I remember I loved prisma because it felt super snappy and fast. I truly want to support prisma and nexus. But you have to give us option for customization :D .

We are using Prisma and Nexus for our app's admin API. All requests are secured by authentication, so t.prismaFields([*]) isn't a security issue. Now this method is deprecated and we have to write thousands of lines of code to upgrade to the latest nexus. This seems like a huge degradation that stops us from upgrading. Agree with @nhuesmann that naming it smth like UNSAFE_... would be enough to attract users' attention to security concerns.

Now this method is deprecated and we have to write thousands of lines of code to upgrade to the latest nexus.

@yantakus You can, with Prisma 2, achieve basically the same thing by just exposing the auto-generated CRUD operations like this so you wouldn't have to write everything again if all you were doing in the first place was exposing the auto-generated operations. I guess you'd just have to pick and choose what you want to expose as opposed to just exposing everything.

export const Query = queryType({
  definition(t) {
    t.crud.products()
    t.crud.somethingElse()
    // ...
  },
})

export const Mutation = mutationType({
  definition(t) {
    t.crud.createOneProduct()
    // ...
  }
})

Prior to Prisma 1, you could achieve that with the following (as you've already done):

const Query = prismaObjectType({
  name: 'Query',
  definition(t) {
    t.prismaFields(['products', 'somethingElse'])
  }
})

You can then use graphql-shield, as in the Prisma 2 examples, to authenticate the different queries and mutations. All that said, I spoke to one of the developers at Prisma and was told that exposing the CRUD operations like this _will_ work to achieve this but that this may not always be the case. For context, I had the same concern you did. The thing that got me here, though, is that I had to also generate all my nexus types using create-nexus-type otherwise it'd complain that the type was missing (and if I forgot to import it to the root, etc.).

@darrylyoung just exposing existing crud operation like in your example in our case is thousands of lines of code, because we have a huge schema.

create-nexus-types could be an option, but it doesn't seem to support Nexus framework (I mean v0.20+ that has breaking changes).

@yantakus create-nexus-types now support two versions of nexus (nexus schema and nexus framework)

I just wanted to say +1 to all of this. We are during the middle of a migration to AWS and we are looking at frameworks to implement this as quickly as possible, so reducing boilerplate is huge for us. I started looking at some alternatives and finally found this video which immediately sold me on Prisma: /watch?v=1qB8vQwWwIc

The issue is that as soon as I started with prisma2, I was completely confused by the documentation, and could not find a proper guide to do all of this. What seemed to be magic in 15 minutes on the video, has been translated into "I have no idea what to do next".

I found this issue by accident where I finally understood that some guides that are over the internet are referencing old repos, old modules, old imports so I am hugely stuck in what is supposed to be a get started tutorial. This is a very steep learning curve which hurts adoption.

Sorry to bump the issue but I was thinking what is the current situation and what are forced to accept as the solution when migrating from prisma 1 to prisma 2.

Basically I have a schema file, with user and their sensitive password that we should not expose.

model User {
  id    String @id @default(cuid())
  email String  @unique
  name  String?
  password String
}

model Text {
  id    String @id @default(cuid())
  data  String?
}

If it was prisma 1, it would auto generate me the important data, and basically to hide the password, I could very easily redefine User in a line, without mentioning anything about the Text model.

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

# we want to hide password, so let's redefine it
type User {
  email: String!
  id: String!
  name: String
}

type Query {
  users: [User!]!
  user(id: ID!): User
  texts: [Text!]!
  text(id: ID!): Text
}

So the new solution is to

  • manually provide everything mentioned in prisma.graphql, which really doesn't make any sense
  • download old generated schema from playground or using a tool
  • manually provide all types using a t.crud.id() style, for every possible model we need, including the one that we already defined.
const User = objectType({
  name: 'User',
  definition(t) {
    t.model.id()
    t.model.name()
    t.model.email()
  },
})

const Text = objectType({
  name: 'Text',
  definition(t) {
    t.model.id()
    t.model.data()
  },
})

const Query = objectType({
  name: 'Query',
  definition(t) {
    t.crud.post();
    t.crud.text();
    // and many more lines with lots of repetation
  }
})

// more stuff here

makeSchema({
  types: [Query, Mutation, Post, User]
})

All these just to hide one line, which was super easy to do before.

We have to migrate a big app with at least hundreds of models, from prisma 1 to prisma 2 because prisma 1 has lots of limitations and performance issues.

So I have few options now,

  • convert the whole thing into nexus style, where I have to define them in prisma file and then again in nexus (minus the password field)
  • write a migration script ourselves which will generate the fields somehow.
  • or migrate to hasura, postgraphile or something else since I have to rewrite the code anyway in new prisma 2 style in lots of places.

_PS: I am not angry, I am just disappointed that I chose and recommended prisma to all those people around me and everyone is blaming me for the super hard learning curve during migration. It's bad that this issue is open for an year without any solution._

Anyway, looking for a magic solution is not worth the time, let's create our own solution for this. :D , for now I am using this piece of code to generate them.

Also, this magic tool is helpful, https://github.com/paljs/prisma-tools

Hi @entrptaher
I was in this same situation until someone tell me about paljs.com
Works like a charm for me and I finally could migrate to prisma2
Basically, I used the command pal generate and choose the output to SDL because I don't want to learn Nexus for now.

This example helps me a lot apollo-sdl-first

I wrote this TypeScript version of the workarounds above:

const exposeAllFields = <T extends string>(
  t: ObjectDefinitionBlock<T>,
  { except = [] }: { except?: (keyof ObjectDefinitionBlock<T>["model"])[] } = {},
) => {
  Object.entries(t.model)
    .filter(([fieldName, _expose]) => !(fieldName in except))
    .forEach(([_fieldName, expose]) => expose());
};

Usage example:

const Model = objectType({
  name: "Model",
  definition(t) {
    exposeAllFields(t, { except: ["secret"] });
  },
});

Type checking works great:
image

It could have been great if @sargunv 's solution was part of the ObjectDefinitionBlock (t) API.

Was this page helpful?
0 / 5 - 0 ratings