graphql-nexus / nexus-prisma

Prisma plugin for Nexus
https://graphql-nexus.github.io/nexus-prisma
MIT License
564 stars 45 forks source link

Possibility to change generated model names #102

Open huv1k opened 3 years ago

huv1k commented 3 years ago

We want to use nexus-prisma for our GraphQL server, but we need to adjust how it generates models. We have 170+ data models and it would require a lot of handwork to manually remap each data model. Currently, we have the name of tables as notes and we want to expose models as Note. Right now we can do something like this

export const Note = objectType({
  name: 'notes',
  definition: (t) => {
    t.field(notes.id);
    t.field(notes.name);
    t.field(notes.snippets);
  },
});

We would like to do something like this:

export const Note = objectType({
  name: 'Note',
  definition: (t) => {
    t.field(Note.id);
    t.field(Note.name);
    t.field(Note.snippets);
  },
});

Ideas / Proposed Solution(s)

I would like to enhance nexus-generator with model mapping and this would enable rename generated names of models.

generator nexusPrisma {
  provider = "nexus-prisma"
  modelMapping = [{from: "notes", to: "Note"}]
}

model notes {
  id   Int    @id @default(autoincrement())
  name String
}

I don't know what could be ideal mapping object and where should configuration live.

Manubi commented 3 years ago

If I understand you correctly you could rename the model to Note and map it to @@(name: "notes")

https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference/#map-1

sachinraja commented 3 years ago

This is similar to @@map, but I believe @huv1k wants it only to apply to the nexus models. Maybe there could be another attribute called @@nexus_map?

jasonkuhrt commented 3 years ago

I would like to consider this problem now as an incremental test/progress of forthcoming Nexus Prisma issues.

This issue feels modest and shouldn't be too hard to implement.

But doing it will stretch both the internal implementation and design thinking hopefully in ways that prepare for features demanding significantly more dynamanism later.

Q&A

Should this setting be runtime or gentime?

The tl;dr is gentime. If you're curious why open up but I think this will be boring to most people :) I think gentime. Consider this: ```ts import { Foo, $settings } from 'nexus-prisma' console.log(Foo.$name) // 'Foo' $settings({ nameMap: { Foo: 'Qux', }, }) console.log(Foo.$name) // 'Qux' ``` I don't see a use-case for this. Runtime opens up the possibility of something wacky like some process input being able to influene Nexus Prisma name map. But this makes no sense to me. Even if some remote data source is desired to be used to drive the name map (e.g. JSON on S3 managed by a CMS controlled by another team???) this is still something that gentime supports. The use-cases exclusive to runtime seem weird and out of scope. Meanwhile, I see added complexity. Runtime level settings mean that application code will need to be aware of if it is accessing types before or after settings have been changed. And this isn't some ordering that's unlikely to be hit. It is typical I think of a Nexus project to statically define GraphQL types at the module level. The developer would need to ensure that their settings side-effect ran before such modules were imported. That kind of thing is error prone, or at least annoying, I think. The only benefit I see of runtime based settings is that they are simpler in the sense that the developer doesn't need to create a new configuration file. Also that configuration file requries them to install `ts-node` (currently at least). So in some ways it might feel "heavier" to have this decoupled settings code and file and maybe extra dep the developer needed to install. 1. `npm add ts-node` 1. `touch prisma/nexus-prisma.ts` 1. New code: ```ts // prisma/nexus-prisma.ts import { settings } from 'nexus-prisma/generator' settings({ nameMap: { Foo: `Qux`, }, }) ``` ```ts // index.ts import { Foo, $settings } from 'nexus-prisma' console.log(Foo.$name) // 'Qux' ``` Actually, another benefit of runtime settings is that they are quicker to iterate upon. Every change to the gentime settings will require another `prisma generate` run. An advantage of the getime is how it plays with static types. For example the type of `Foo.$name` isn't just `string` but `'Foo'`. So if we open runtime settings its going to require rethinking those types and/or runtime-typegen concepts (ala Nexus) which brings on more complexity in turn. This point gets more problematic when considering relations. Consider `Foo.bar` where `bar` field type is `Bar`, a relation. If at runtime `Bar` is mapped to `Qux` then `t.field(Foo.bar)` is no longer going to work from a static typing perspective. Nexus will know about a type called `Qux` in its typegen, meanwhile, `Foo.bar.type` will still be the old _static type_ `'Bar'`. Maybe runtime typegen ala Nexus will eventually be worth it but this isn't it. I see a lot of work down this path for little gain for Nexus Prisma users. The gentime settings are fine, good, great, to use and avoid this problem. So in conclusion: | Gentime Pros | Runtime Pros | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | No runtime side-effect ordering issues | fast feedback loop (no prisma generate required) | | No new layer of typegen required (runtime typegen ala Nexus) | Simpler project setup: 1) no new file needed 2) no ts-node needed 3) code can be co-located | Those Gentime pros seem far nicer to me than the Runtime ones.

What should the API be like?

As a start I'm thinking this:

settings({
  nameMap: {
    '<Prisma Model Name>': '<Desired GraphQL Type Name>',
  },
})

But there might be patterns that the developer wants to automate. Not only is that a big time saver its helpful for maintenance and communicating with team members in the sense that patterns encode a kind of intent, whereas the pure static approach might hide it (actual results here vary by complexity/sensability of pattern etc.).

So imagine this:

settings({
  nameMap: {
    static: {
      '<Prisma Model Name>': '<Desired GraphQL Type Name>',
    },
    patterns: {
      '<RegExp with Capturing Groups>':
        '<Desired GraphQL Type name maybe using capturing groups $1 $2 $3>',
    },
  },
})

There are Prisma Schema characteristics that a developer might want to draw upon. If we think of the above so far as a shorthand, here's how more sophisticated patterns might be expressed:

settings({
    nameMap: {
        static: {
            "<Prisma Model Name>": "<Desired GraphQL Type Name>",
        },
        patterns: [{
            {
                description: 'Optional pretty title here',
                matchName: "<RegExp with Capturing Groups>",
                matchModels: boolean
                matchEnums: boolean
                projectAs: "<Desired GraphQL Type name maybe using capturing groups $1 $2 $3>",
            }
        }]
    },
})

We could use Prisma types to give autocomplete for the static name map.

We could use some helpful validation to sanity check things like:

Static name maps would overrule patterns.

We could consider an API instead of configuration. At some point it all boils down to configuration so I see the following as sugar. But its more than just addidtive. Choices in the API may motivate removing sugar from the configuration schema. Let's see.

settings({
  transformations: [
    map.names.static('<Prisma Model Name>', '<Desired GraphQL Type Name>'),
    map.names.static({
      '<Prisma Model Name>': '<Desired GraphQL Type Name>',
    }),
    map.names.pattern(
      '<RegExp with Capturing Groups>',
      '<Desired GraphQL Type name maybe using capturing groups $1 $2 $3>'
    ),
    map.names.pattern({
      prisma: '<RegExp with Capturing Groups>',
      graphql:
        '<Desired GraphQL Type name maybe using capturing groups $1 $2 $3>',
      description: 'Optional pretty title here',
      includeModels: boolean,
      includeEnums: boolean,
    }),
  ],
})

Maybe a chaining API?:

settings({
  transformations: [
    map.names.static({
      '<Prisma Model Name>': '<Desired GraphQL Type Name>',
    }),
    map.names.pattern(
      '<RegExp with Capturing Groups>',
      '<Desired GraphQL Type name maybe using capturing groups $1 $2 $3>'
    ),
    map.names
      .pattern()
      .description('Optional pretty title here')
      .prisma('<RegExp with Capturing Groups>')
      .graphql(
        '<Desired GraphQL Type name maybe using capturing groups $1 $2 $3>'
      )
      .models(false),
  ],
})

And we could also get away from the settings configuration object to clean things up further:

import { map } from 'nexus-prisma/generator/settings'

map.names.static({
  '<Prisma Model Name>': '<Desired GraphQL Type Name>',
})

map.names.pattern(
  '<RegExp with Capturing Groups>',
  '<Desired GraphQL Type name maybe using capturing groups $1 $2 $3>'
)

map.names
  .pattern()
  .description('Optional pretty title here')
  .prisma('<RegExp with Capturing Groups>')
  .graphql('<Desired GraphQL Type name maybe using capturing groups $1 $2 $3>')
  .models(false)

I could imagine a lot more functionality extending this over time. Imagine to reduce risk the developer wanted to omit sensitive fields from being projected.

import { omit } from 'nexus-prisma/generator/settings'

omit.fields
  .at('Foo.bar')
  .at('*.password')

omit.fields
  .pattern(/^password$|^.+Password$/)
  .description('Never project passwords')

Back to name mapping... maybe we merge .prisma with .pattern constructor, but we lose the .prisma/.graphql symmetry then.

import { map } from 'nexus-prisma/generator/settings'

map.names.table({
  '<Prisma Model Name>': '<Desired GraphQL Type Name>',
})

map.names
  .pattern('<RegExp with Capturing Groups>')
  .graphql('<Desired GraphQL Type name maybe using capturing groups $1 $2 $3>')
  .description('Optional pretty title here')
  .enums(false)

I am intrigued by the API approach. I think it might be pretty nice to work with and scale up to some of the quite complex problems that we encountered when designing the CRUD aspects of the previous version of Nexus Prisma.


I'll leave these thoughts here for a bit, please leave feedback if you have any, its welcome!

jasonkuhrt commented 3 years ago

Thinking we could have a selector API.

Also rename description to comment.

Also drop enum(true) in favour of clearer only(...) and skip(...) APIs.

import { map, $ } from 'nexus-prisma/generator/settings'

map.names.table({
  '<Prisma Model Name>': '<Desired GraphQL Type Name>',
})

const stripeEnums = $.name(/Stripe(.+)/)
  .only({ enum: true })
  .comment('Optional comment here')

map.names.from(stripeModels).to('Fin$1')

Also thining that we can use named capture groups with a sprinkle of typegen to do something like transform capture groups to merge with other text. I think this would be a very common need. In order for this to work the following needs to happen:

  1. Initially to has any params type
  2. During prisma generate NP will analyze all the regexes used
  3. NP will emit some TS typings into node_modules/@types/... based on its findings
  4. NP generator settings module will pick those up and apply it to its API, give global interface stuff
import { map, $ } from 'nexus-prisma/generator/settings'

map.names.table({
  '<Prisma Model Name>': '<Desired GraphQL Type Name>',
})

const stripeEnums = $.name(/Stripe(?<name>.+)/)
  .only({ enum: true })
  .comment('Optional pretty title here')

map.names.from(stripeModels).to(({ name }) => `Fin${pascalCase(name)}`)

However... in order to target the function calls with the right types we'll need a targeting mechanism. A string literal is typically how this is done. But we'll need to tweak the API for that then...

Idea (1) allow a pattern title...

const stripeEnums = $.name(/Stripe(?<name>.+)/)
  .only({ enum: true })
  .comment('Optional pretty title here')

map.names
  .pattern('unique title')
  .from(stripeModels)
  .to(({ name }) => `Fin${pascalCase(name)}`)

Idea (2) force regular expressions to be represented as strings...

const stripeEnums = $.name('Stripe(?<name>.+)')
  .only({ enum: true })
  .comment('Optional pretty title here')

map.names.from(stripeModels).to(({ name }) => `Fin${pascalCase(name)}`)

Idea 2 feels a lot more seamless from an API perspective but the loss of RegExp tooling (at least syntax highlighting but maybe there are IDE plugins etc. too going on) is unfortunate. However it seems the less confusing way forward here.

We could make $ sugar for $.name

const stripeEnums = $('Stripe(?<name>.+)')
  .only({ enum: true })
  .comment('Optional pretty title here')

map.names.from(stripeModels).to((groups) => `Fin${pascalCase(groups.name)}`)

Instead of overloading to we could introduce a transformer:

const stripeEnums = $('Stripe(?<name>.+)')
  .only({ enum: true })
  .comment('Optional pretty title here')

map.names
  .from(stripeModels)
  .transform(({ name }) => ({ name: pascalCase(name) }))
  .to('Fin<name>')

Actually I think the function form of to is still useful to have, but I think transform is a useful addition.

So I would consider the to template form as shordhand for the to function form. Aka. sugar.

Basically .to('Fin<name>') would be sugar for .to((groups) => \Fin${groups.name}`)`.

A downside of to sugar is that it doesn't type check e.g. no static error on 'Fin<naem>' however we could of course do runtime validation on that.

iddan commented 3 years ago

Please no chaining APIs. They are much harder to interact with programmatically.

jasonkuhrt commented 3 years ago

@iddan Will need a stronger reason than that to dismiss it. Examples etc. and counter proposal. Chaining APIs are a primary way to get type safety in many cases too, although things can be achieved with a pipe(funcA, funcB, funcC) style too I'm less familiar with it and there is less discoverability built into that API.

E.g. this:

However... in order to target the function calls with the right types we'll need a targeting mechanism. A string literal is typically how this is done. But we'll need to tweak the API for that then...

Which parts of the chaining API are actual separate combinators vs which are separated config steps is also another aspect to consider.

Don't get me wrong, I'm all for a statics set of combinators that can be accessed for "programatic" usage (quotes b/c there is not enough information here yet to conclude the chaining API doesn't serve that) but I would not get rid of the chaining API for it probably, at the very least because right now we don't actually know what it means/is/works semantically.

import { $ } from 'nexus-prisma/generator/settings'

$.funcA().funcB().funcC()
import { funcA, funcB, funcC, pipe } from 'nexus-prisma/generator/settings'

pipe(funcA(), funcB(), funcC())

I don't think we should waste too many cycles about which style is better right now. More important to focus on what the primitives even are, what mutates, what is immutable, etc.

I'll probably for now and would suggest to others to just alternate between the styles to keep a lightweight fresh perspectives of ergonomics.

villesau commented 2 years ago

I think this kind of feature would largely cover my proposal for relay connection support: https://github.com/prisma/nexus-prisma/issues/212 Or at least I believe it should be possible to build some utilities on top of this to support that.