Closed quintal-william closed 2 years ago
Hi @saphewilliam Thanks for the first suggestion of this library :D
I was wondering if this would be the best way to solve this type of problem using this codegen...
I think that if we do something like this just in order to avoid having to modify the generated code, this project will "solve half the problem"
We could add things like:
But we would probably still have to modify it by hand in some cases, such as:
So in your case, I believe that after the project has been generated, the best option would be to manually modify the "password" field.
When the file is generated, you look at the diffs, and decide what to keep/change.
If you no longer want to generate new versions of the file, you can add it to the "ignore" list in the options.
But if this option seems bad to you, there is a way to solve it using the "replacer" function, inside the options.
With it, you can freely modify the generated code, so you don't always have to manually update it.
Here's an example of how you would comment out the user's password line automatically:
const hideUserPasswordField= (generated: string) => {
return generated.replace(/(export const User = builder\.prismaObject(?:(?:.|\n)*))(password:)/gm, '$1// $2')
}
export const configs: ConfigsOptions = {
crud: {
....
},
inputs: {
...
},
global: {
replacer: (generated, position) => {
const withoutPassword = hideUserPasswordField(generated)
return withoutPassword
}
}
}
Does this solve your use case?
Edit
I believe that this functionality could come in well in the case of inputs (the input file is not made to be changed) and having to do these "replacer" functions will cost a lot.
If you have time to do it, I will accept your pull request.
If not, I believe in 2 weeks, I will implement.
Until then, I will add this functionality to the roadmap in the readme.
You raise a good point regarding authentication and any other extensions (e.g. query complexity or errors) that you might want to add to a crud output. However I personally don't like the solution of manually checking diffs, and the replacer feels more like a workaround. It would probably be too time consuming and complex to add in this central location for all fields you might want to hide for your models.
I think that there is an api, perhaps similar to the old nexus-prisma-plugin api, that could be the best of both worlds: a distributed way to generate the code you want to generate with the ability to override it or easily add to it.
For reference, the old nexus-plugin-prisma api:
import { schema } from 'nexus'
schema.objectType({
name: 'User',
definition(t) {
t.model.id()
t.model.name()
t.string("uppercaseName", {
resolve({ name }, args, ctx) {
return name.toUpperCase(),
}
})
t.model.posts()
},
})
schema.queryType({
definition(t) {
t.crud.user()
t.crud.users({
ordering: true,
filterint: true,
}),
},
})
schema.mutationType({
definition(t) {
t.crud.createOneUser()
t.crud.deleteOneUser()
},
})
I have not used Pothos yet, would have to learn about it if I were to help with implementing this, but I feel like if we were to implement an API similar to that, we could have something great. A possible idea (correct me if I'm wrong and if this would be impossible to implement, I'm spitballing right now. Apologies for any syntax errors, I'm typing this outside of an IDE):
// We expose generated utilities with a naming convention similar to Prisma
import { User, findUniqueUser, createUser } from "@/graphql/generated"
// To expose a query or mutation, we define a query type as usual, and only expose what we want using the utilities.
builder.queryType({
fields: (t) => ({
// These utilities can be called, passing in the t argument and a configuration object.
// The config object can add extensions (error, complexity, authorization) and can override the resolve function.
// You cannot change or alter the args or type of the resolver through config.
user: findUniqueUser(t, { ...config })
})
})
// To expose an object type, we can do any of the following with increasing levels of customization:
// Fully auto-generated
builder.prismaObject('User', User)
// Auto-generated, but ability to add to or omit from the model
// Bonus points if the omit array could be type-safe
builder.prismaObject('User', { fields: User.fields(t, {
omit: ['password'],
extend: (t) => ({
bio: t.string({
select: { profile: { select: { bio: true } } },
resolve: (user) => user.profile.bio,
}),
}),
})})
// Complete field-level control, similar to the proposed crud utilities
builder.prismaObject('User', {
fields: (t) => ({
id: User.id(t, { ...config })
})
})
The findUniqueUser
-type crud utilities would call t.prismaField()
internally, providing the type, a default resolve function, already plugged in pre-generated input args, and it would spread the given configuration after those defaults:
t.prismaField({
type: User,
nullable: true,
args: { where: t.arg({ type: UserWhereUniqueInput, required: true }) },
resolve: async (query, root, args, ctx, info) => await ctx.prisma.user.findUnique(...args, ...query),
...config,
})
The User
-type model utilities would look something like this internally:
{
fields: (t, config?: { omit: string[]; extend: (t) => object; }) => {
// Function that exposes all fields of the object, only if the field name is not present in the config.omit array.
let result = {};
if (!config.omit && !config.omit.includes('id')) result = {...result, id: (t, config) => t.exposeID('id', {...etc})}
if (config.extend) result = { ...result, ...config.extend(t) }
return result;
},
// The individual model fields for further customization
id: (t, config) => t.exposeID('id', {...etc}),
}
Then, if we only implement the API I suggested in my initial comment (or something similar) to input type generation, then I believe we would have the best of both worlds: completely customizable input type generation + control over the output types and the generated crud.
I think a lot of people in that nexus-prisma
thread (including me) would instantly jump ship to Pothos if this could all work, but I'd definitely love to hear your thoughts!
@saphewilliam What you are looking for is exactly what I asked the Pothos creator in their "crud" plugin, which is under development (But, he doesn't intend to do anything along that line)
I created this lib to partially solve the problem
I just now realized that this codegen can solve the same problem (and not depend on a Pothos plugin):
We can generate 2 files, one that will always be faithful to the Prisma, and another that will be created to be extended
What do you think of this solution?
I really like it! It provides sensible defaults with full possibility of overriding them if necessary, and I don't think it would require a big rewrite of your current code.
I have a few points I'd like to discuss further:
Is there any particular reason you're choosing to spread the CommentObject
instead of just passing it in like so?
// @/graphql/Comment/object.ts
import { CommentObject } from "@/graphql/__generated__/objects.ts";
export const Comment = builder.prismaObject('Comment', CommentObject)
Personally, I like to split my generated code from my written code, which I think is clearer and allows me to put other things related to the model in the written code directory, resulting in a directory structure like this:
- graphql
- __generated__
- inputs.ts
- objects.ts
- Comment # the generated Comment code
- mutations
- createMany.ts
- createOne.ts
- ...
- queries
- count.ts
- findFirst.ts
- ...
- index.ts
- object.ts
- Comment # the written Comment code
- object.ts # the object definition
- mutation.ts # the mutations related to the object
- mutations.graphql # the mutations used by the frontend
- query.ts # the queries related to the object
- queries.graphql # the queries used by the frontend
- schema.prisma # prisma schema of the model (merged using prismerge)
- seed.ts # data seeding for the model
Long story short, I think it's a good idea to be unopinionated about the directory structure the developer chooses. I think naming all generated files *.base.ts
is good, so that only files ending in that postfix are removed when code is regenerated. All other files that the developer puts in that directory just stay where they are. I don't think it's necessary to auto-generate files like object.ts
from your previous comment, the developer can create this file themselves wherever they like.
/// @Pothos.omit()
in the Prisma schema to omit certain fields from generated input and/or object code (as suggested in my first comment)? Some examples: @Pothos.omit(create, update)
omits the field from the create and update inputs, @Pothos.omit(input)
omits the field from all inputs, @Pothos.omit(object)
omits the field from the generated object definition, and @Pothos.omit()
omits the field from all inputs and the object definition.@saphewilliam
I really like it! It provides sensible defaults with full possibility of overriding them if necessary, and I don't think it would require a big rewrite of your current code.
I agree e.e
- Is there any particular reason you're choosing to spread the CommentObject instead of just passing it in like so?
No, its only for example e.e
- Personally, I like to split my generated code from my written code, which I think is clearer and allows me to put other things related to the model in the written code directory, resulting in a directory structure like this: ...
I like the ideia for "unopinionated folder structure". We could have the more options.
I don't think it's necessary to auto-generate files like object.ts from your previous comment, the developer can create this file themselves wherever they like.
Can be a option too.
I think naming all generated files *.base.ts is good, so that only files ending in that postfix are removed when code is regenerated.
I don't understand the "removed"
Example of options:
export const configs: ConfigsOptions = {
crud: {
outputFolderPath: "./generated", // Keep it as global, if users want to create all the files in same folder
outputFolderPathBaseFiles: "./graphql/__generated__", // generating base files in a speciifc location, default: undefined
outputFolderPathSrcFiles: "./graphql", // generating SRC files in a speciifc location, default: undefined
baseFileName: "object.ts", // default: #{model}.base.ts
baseOnly: true, // dont generate src files. default: false
mergeResolvers: true, // merge all queries in queries.ts, merge all mutations in mutations.ts
},
inputs: {
...
},
global: {
...
}
}
I thought and there is not much reason to have a "base" for "resolvers". That's because, they never need to be regenerated. (What do you think?)
NOTE: I still don't know exactly how to export the typed base to resolvers, because pothos typing is quite complex. The pothos author helpedme to create a base for objects.
Example generated file using the options above:
- graphql
- __generated__
- inputs.ts // is always overwritten
- objects.ts // is always overwritten
- Comment
- object.ts // is always overwritten
- index.ts // is always overwritten
baseOnly
: falsemergeResolvers
: true- graphql
- __generated__
- inputs.ts // is always overwritten
- objects.ts // is always overwritten
- Comment
- object.ts // is always overwritten
- index.ts // is always overwritten
- Comment
- object.ts // generated, but never replaced
- mutations.ts // generated, but never replaced
- queries.ts // generated, but never replaced
- index.ts // generated, but never replaced
baseOnly
: falsemergeResolvers
: false- graphql
- __generated__
- inputs.ts // is always overwritten
- objects.ts // is always overwritten
- Comment
- object.ts // is always overwritten
- index.ts // is always overwritten
- Comment
- queries
- findFirst.ts // generated, but never replaced
- count.ts // generated, but never replaced
- ...
- mutations
- ...
- object.ts // generated, but never replaced
- index.ts // generated, but never replaced
How do you feel about the idea of /// @Pothos.omit() ...
Necessary, and it will be the first of these things that I will develop
So the roadmap for this issue:
Hi! Just wanted to jump in and ask for the progress on this feature? I have some free time in the coming week which I could spend on this project. Have you started anything yet / would you like me to?
@saphewilliam Sorry, I haven't been able to make progress with this functionality yet. Any contribution will be welcome :D
Hi @saphewilliam Thank you very much for your great contribution.
I believe that a last important improvement would be to be able to generate "everything".
Your comment says:
I don't think it's necessary to auto-generate files like object.ts from your previous comment, the developer can create this file themselves wherever they like.
I agree, but an option to "generate only the base files", could make it possible to create only the files to be imported, without creating the Queries, Mutations and Objects.
And in cases where necessary, the library could generate the entire project.
Thanks so much! It was a blast trying to tap into the Pothos type system for this project. I learned a lot!
So you're suggesting adding a crud.generateBaseOnly: boolean
(default: false
) option to the config which, if set to false, would create (if not exists) the object.ts and all queries and mutations? What would these files look like? Some examples:
// ./User/query.ts or ./User/queries.ts
export const findManyUser = builder.queryFields(findManyUserQuery);
export const findUniqueUser = builder.queryFields(findUniqueUserQuery);
// All other queries
or
export const findManyUser = builder.queryFields((t) => ({
findManyUser: t.prismaField(findManyUserQueryObject(t),
}));
export const findUniqueUser = builder.queryFields((t) => ({
findUniqueUser: t.prismaField(findUniqueUserQueryObject(t),
}));
// All other queries
or
export const userQuery = builder.queryFields((t) => ({
findManyUser: t.prismaField(findManyUserQueryObject(t)),
findUniqueUser: t.prismaField(findUniqueUserQueryObject(t)),
// All other queries
}));
or maybe even
export const userQuery = builder.queryFields((t) => {
const findManyUser = findManyUserQueryObject(t);
const findUniqueUser = findUniqueUserQueryObject(t);
// All other queries
return {
findManyUser: t.prismaField({ ...findManyUser }),
findUniqueUser: t.prismaField({ ...findUniqueUser }),
// All other queries
}
});
And how does the object.ts look? Do we assume that the user would want to edit the generated code? Then we should generate it such that it is easy to start editing (that would probably be the last option?)
Edit: whoops accidentally submitted too early
Thanks so much! It was a blast trying to tap into the Pothos type system for this project. I learned a lot!
What you did with the typing was really insane.
So you're suggesting adding a crud.generateBaseOnly: boolean (default: false) option to the config which, if set to false, would create (if not exists) the object.ts and all queries and mutations? What would these files look like? Some examples:
Yes. I believe this project should be the fastest way to turn schema.prisma
into a full Crud
And how does the object.ts look? Do we assume that the user would want to edit the generated code? Then we should generate it such that it is easy to start editing (that would probably be the last option?)
The last option is perfect, what do you think?
Okay let's use the last option! Do you have time or should I start working on it? 😃
Okay let's use the last option! Do you have time or should I start working on it? 😃
I think in a few days I'll make it, finally xD If you are interested in doing it, feel completely free (I sent you an invitation to be a collaborator, if you want e.e)
What you are looking for is exactly what https://github.com/hayes/pothos/pull/348#issuecomment-1174231055 the Pothos creator in their "crud" plugin
We are getting very close to my initial proposal for the creator of Pothos
I believe that in the near future, we could export a function from within objects.ts to auto generate the entire project, with exceptions
, like this:
builder.prismaCrud()
// 2. SPECIFIC: Generating only models, queries or mutations
// src/schema/index.ts
builder.queryFields((t) => t.prismaCrud() // findCount, findFirst, findMany, findUnique, aggregate for all models
builder.mutationFields((t) => t.prismaCrud() // createOne, createMany, deleteMany, deleteOne, updateOne, updateMany, upsertOne for all models
builder.prismaCrudObjects() // create all objects, with all fields
// Auto generate for specific Models
builder.queryFields((t) => t.prismaCrud({ include: ["Post"] }) // operations for User only.
builder.mutationFields((t) => t.prismaCrud({ exclude: ["User"] }) // operations for all but User.
builder.prismaCrudObjects({ exclude: ["User"] }) // create all objects but User, with all fields
In our case it would be something like this
const objectsNames = ["Profile", "User", ...]
export function generateAllObjects ({ include, exclude }: { include: string, exclude: string }) {
objectsNames
.filter...
.map(object => {
builder.prismaObject(object, Objects[`${object}Object`]); // Objects is all imports
})
}
export function generateAllQueries ...
export function generateAllMutations ...
export function generateAllCrud() {
generateAllObjects()
generateAllQueries()
generateAllMutations()
}
What do you think?
I have some other stuff to work on in the coming days so it would be nice if you could pick this up!
I personally probably would not use those kinds of functions, but I understand the appeal of setting up an entire API prototype at lightspeed and then refining it model by model.
include: string[]
should be include: keyof Types['PrismaTypes'][]
, etc. { include: ['Post'], exclude: ['User'] }
or even { include: ['Post'], exclude: ['Post'] }
? I would suggest typing the generateAllObjects
function something like this to avoid this type of misuse and confusion:
const generateAllObjects: <Type extends keyof Types['PrismaTypes']>(opts: { include: Type[] } | { exclude: Type[] }) => void = ...`
PS: what are the odds that we could use this current codebase to make an actual Pothos plugin to do this, not just a codegen. I think we're not very far off. Perhaps something to think about / experiment with / open a new issue about in the near future.
PPS: Hayes opened a first version of the @pothos/plugin-prisma-utils plugin, which we should probably keep an eye out for.
I'll get into this today, so I can release version 0.4.0
About the code, thanks for the suggestions. 😄
About Pothos Plugin, it was the first thing I thought of at the beginning, but from what I saw when I studied the plugin codes a little, it would be quite complex for my current knowledge of Typescript.
Now, we have a autocrud.ts
autogenerated file (optionally disableable) that exports generateAllObjects
, generateAllQueries
, generateAllMutations
, generateAllCrud
functions to auto generate all crud (example are updated).
And now we've got back the status of 'automatically generate crud'. I believe generating object.ts
will no longer be necessary for now.
Very happy with the end result. good job ^^ Closing this issue, new version 0.4.0 are published.
First of all, I love what you're doing here. I'd definitely see myself using this tool for a project I'm working on. To be able to use it, I would like to see a feature added to your tool. I'd be willing to submit a PR if you're tight on time, but I'd like to hear your thoughts first:
I'd like to be able to hide certain fields at certain points in the graphql schema. Typegraphql-prisma provides this feature with an API I personally enjoy: https://prisma.typegraphql.com/docs/advanced/hiding-field
Where the values for
output
may betrue
(to omit in all output cases) or("create" | "update" | "where" | "orderBy")[]
If I were to change anything about this api, it would be to:
@Pothos.omit()
be valid, meaning "hide this field everywhere""create" | "update" | "where" | "orderBy"
in quotes if possible (e.g.@Pothos.omit(input: [create, update])
)input
andoutput
as keys in an array of things you would want to omit (e.g.@Pothos.omit([create, update, output])
, or@Pothos.omit([input])
)Would love to hear your thoughts and if you would have time to implement this or a similar feature anytime soon!