Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possibility to change generated model names #102

Open
huv1k opened this issue Jul 21, 2021 · 7 comments
Open

Possibility to change generated model names #102

huv1k opened this issue Jul 21, 2021 · 7 comments
Labels
type/feat Add a new capability or enhance an existing one

Comments

@huv1k
Copy link

huv1k commented Jul 21, 2021

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.

@huv1k huv1k added the type/feat Add a new capability or enhance an existing one label Jul 21, 2021
@Manubi
Copy link

Manubi commented Jul 26, 2021

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
Copy link

sachinraja commented Jul 27, 2021

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 jasonkuhrt pinned this issue Aug 30, 2021
@jasonkuhrt
Copy link
Member

jasonkuhrt commented Aug 30, 2021

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:

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
  2. touch prisma/nexus-prisma.ts
  3. New code:
// prisma/nexus-prisma.ts

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

settings({
  nameMap: {
    Foo: `Qux`,
  },
})
// 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:

  • Find patterns that match nothing
  • Find patterns that conflict

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
Copy link
Member

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
Copy link
Collaborator

iddan commented Aug 31, 2021

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

@jasonkuhrt
Copy link
Member

jasonkuhrt commented Aug 31, 2021

@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.

@jasonkuhrt jasonkuhrt unpinned this issue Oct 14, 2021
@jasonkuhrt jasonkuhrt pinned this issue Dec 15, 2021
@villesau
Copy link

villesau commented May 3, 2022

I think this kind of feature would largely cover my proposal for relay connection support: #212 Or at least I believe it should be possible to build some utilities on top of this to support that.

@rostislav-simonik rostislav-simonik unpinned this issue Oct 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type/feat Add a new capability or enhance an existing one
Projects
None yet
Development

No branches or pull requests

6 participants