-
-
Notifications
You must be signed in to change notification settings - Fork 438
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
[Feat] CRUD and Nested Mutations #131
Comments
For your question about generating code, you should look into making a custom directive. Then you can make that pretty much do whatever you want. |
Hey @spawnia, I was originally going to do something like this for v2.0 but it quickly spiraled into a much bigger issue than I originally thought. I've built on my initial ideas and I could see a nested implementation that would look something like this: # The `crud` argument tells Lighthouse to create crud mutations for this model.
#
# The `connect` argument allows you to specify which
# nested relationships you can include in a auto-generated
# crud mutation.
#
# The @hidden directive prevents these fields from being included in the final schema,
# the fields are just there for validation purposes.
#
type User @model(
class: "App\\User",
crud: true,
connect: ["posts"]
) {
id: ID!
name: String @validate(rules: ["required"])
email: String @validate(rules: ["required", "emails", "unique:users,email"])
password: String @hidden @validate(rules: ["required", "min:8", "confirmed"])
password_confirmation: String @hidden
posts: [Post] @hasMany
}
# The `validators` argument allows you to specify validator
# classes for the `create`, `update`, or `delete` crud mutations.
#
# The `policies` argument allows you to specify a Laravel policy for
# each crud mutation
#
# The `transformers` argument allows you to transform the input before
# it gets injected into the model.
#
type Post @model(
class: "App\\Post",
crud: true,
validators: {
# create: "App\\GraphQL\\Validators\\CreatePostValidator" -- Handled by @validate below
update: "App\\GraphQL\\Validators\\UpdatePostValidator"
},
policies: {
create: "create",
update: "update",
delete: "delete"
},
transformers: {
create: "App\\GraphQL\\Transfomers\\PostTransformer@create",
update: "App\\GraphQL\\Transfomers\\PostTransformer@update",
}
) {
id: ID!
title: String @validate(rules: ["required", "min:4"])
body: String @validate(rules: ["required", "min:4"])
author: User
} There's a ton going on here, but this is something I think could work for most use-cases while also allowing for custom validation and injecting policies when needed. The Right now, the biggest problem is getting proper validation error messages when you're creating an array of nested relationships. I labeled this issue as |
Thank you for your quick answer, i like what you planned so far. It clearly shows that you have been thinking about this a lot. What i would suggest for a first working version is to leave out the additional configuration options for We might be able to cut down on the necessary configuration even more if we have the While i get the intent behind the On a side note, i think that validating with There are some cases where it might be sensible to add in validation rules such as Below would be an example of a stripped down type definition. type User @model(class: "App\\User") {
id: ID!
email: String! @validate(rules: ["email", "unique:users,email"])
password: String! @hidden @validate(rules: ["min:8"])
posts: [Post!] @hasMany
}
type Post @model(class: "App\\Post") {
id: ID!
title: String! @validate(rules: ["min:4"])
body: String @validate(rules: ["min:4"])
views: Int! @readOnly
author: User! @belongsTo
} And this is what the generated schema would be: # Those two types are used as return types for Queries
# I am not going to spell out the actual Queries since
# i feel that would be out of scope.
# There may be all kinds of filtering, grouping etc. going on.
type User {
id: ID!
email: String!
posts: [Post!]
}
type Post {
id: ID!
title: String!
body: String
views: Int!
author: User!
}
type Mutation {
createUser(data: CreateUser!): User
createPost(data: CreatePost!): Post
updateUser(id: ID!, data: UpdateUserData!): User
updatePost(id: ID!, data: UpdatePostData!): Post
deleteUser(id: ID!): User
deletePost(id: ID!): Post
}
input CreateUser {
email: String!
password: String!
posts: [CreateUserPostRelation!]
}
# Only one of the actions in here is allowed
input CreateUserPostRelation {
connect: ID
create: CreateUserPost
}
input CreateUserPost {
title: String!
body: String
}
input CreatePost {
title: String!
body: String
author: CreatePostAuthorRelation!
}
input CreatePostAuthorRelation {
connect: ID
create: CreatePostAuthor
}
input CreatePostAuthor {
email: String!
password: String!
}
# Similar to the Create inputs, although all fields are optional
# If they were marked as non-null before, we can still
# validate that if a value is given, the value must not be null.
input UpdateUserData {
email: String
password: String
posts: [UpdateUserPostRelation!]
}
input UpdateUserPostRelation {
connect: ID
# If posts were not required to have a user attached we might have:
# disconnect: ID
create: CreateUserPost
update: UpdatePost
delete: ID
}
input UpdatePost {
id: ID!
data: UpdatePostData
}
input UpdatePostData {
title: String
body: String
author: UpdatePostAuthorRelation
}
input UpdatePostAuthorRelation {
connect: ID
create: CreatePostAuthor
update: UpdateUserData
# If author were nullable, we could have:
# disconnect: ID
# delete: ID
} Notice how it is really long? This is exactly the reason why i think this should be generated automatically. It looks a bit ugly, but it offers complex mutations while still keeping the type safety that makes GraphQL so special. |
@spawnia, really appreciate the feedback!!! So, defaulting I should probably set some expectations about how CRUD will work in Lighthouse (at least initially). Inputs like If the primary goal of Lighthouse were to be more of a backend ORM type service such as Prisma, that you wouldn't necessarily provide as an open API to the public (i.e., it would be hidden behind another "frontend" schema), I would be all about this approach! However, in my own project, there's probably one or two instances where I would ever want to allow a foreign key to be changed by the end-user. The CRUD being generated should be thought of more like Laravel's The Thanks again for your feedback on this @spawnia! Great discussion on a pretty big topic!! |
@chrissm79 I like the idea and I think this should be in Lighthouse. However I think we soon need to figure out what we want to be part of Lighthouse and what should be a package. |
@olivernybroe Couldn't agree more! I think it's reasonable to have Lighthouse optionally include some basic CRUD, but beyond that it should probably be a separate package that manages a more complex schema. But that's the beauty of Lighthouse, a complex crud system can be generated with a custom directive! |
Enabling CRUD by defaultI can get behind not defaulting to true in this major release series. Actually, it seems like CRUD in the way i envisioned it would be a great fit for internal or backend-facing API but might add just a little bit too much power to public APIs. I get that creating Users as a related object is something that usually would not be allowed. The example is a bit weak in this regard, it makes a lot more sense in other cases. For example, when creating a post, you might want to connect it to some categories and be able to create new categories in the process. Many times, you will not even expose such options through your UI, but nonetheless it is nice to have a flexible, powerful schema that is able to perform such operations without writing any additional code. If you really do not want someone to perform such actions, there could be an option to explicitely disallow creation or updating as part of a relation. Reasoning behind the nested inputs
Actually, that just follows from a naming convention: This class of RelationInputs helps to keep the schema dry and actually makes for a pretty nice structure when calling the API. This is what a query for creating a User could be like: mutation {
createUser(data: {
email: "ex@amp.le",
password: "safetyfirst",
posts: [
{connect: 42},
{create: {title: "GraphQL is awesome", body: "Especially nested mutations ;)"}
]})
{
id
posts {
id
title
} How to define required argumentsI can definitely see the benefits of using Laravel for validating Inputs as Non-Null. You make some great points for why it is beneficial. In a perfect world, we would be able to change the behaviour of Inclusion into LighthouseI really like what Lighthouse is doing in general and hope to avoid having to write yet another GraphQL implementation for Laravel. There is a bunch of them already and all of them do just a few things well but can not really bring it all together. While it would be quite doable to have multiple packages for serving REST endpoints, the nature of GraphQL makes it advantageous to have it all in one package. The implementation of nested mutations is special in the regard that it has to be integrated quite deeply with how Lighthouse works. Simply defining a model as CRUD has wide-ranging implications: Transactional handling of the mutations, schema generation, validating nested inputs... This makes me think it would be too complex for an extension package. Should the need arise, it might be a good idea to add in some kind of plugin system to Lighthouse or allow easy extensibility with some custom directives. At a certain point, adding all kinds of use cases could be too much for Lighthouse to handle. However i feel that offering complex CRUD operations with little to no additional coding would be a huge benefit and i would really like it as a part of Lighthouse. I have been struggling to find a way to deal with nested mutations for our project, and so far Lighthouse looks like the most promising candidate. If you are interested in implementing this, i can get started on this tommorrow. As this is currently a top priority for our project, i will have plenty of time to work on this in the next few weeks. |
@spawnia It is possible to do this in Lighthouse. I am for example working on a filter directive which generates the filter methods automatically based on the fields and gives support for filtering relationships fields recursively also. All of this is done with a directive. I think when you are making this, you will realise that you don't need to change anything in graphql and this could actually be a package. However I vote for adding this in graphql, but having it disabled by default. |
My experimentation so far has lead to me the following idea: We can introduce a special directive called These directives are handled differently from the other directives. They can only be used on Object Types and are evaluated before all other directives. They are passed the original Document and return a modified version. Most of the time, they will actually remove the type they came on and define multiple other types, fields, inputs, etc. I will expose methods for common schema manipulations (such as adding a field to the Query root type) There are multiple benefits to this approach:
Example: type User @generate(
# This is required when using @generate
class: "App\\User"
# Add all available CRUD operations
crud: true
) {
# List of fields here
}
type Post @generate(
class: "App\Post"
# Generate a Query to get a post by ID and another to Query posts paginated
read: ["single", "paginated"]
# Expose create mutation but not update or delete
create: true
# Those would be the defaults, just adding them in for clarity
update: false
delete: false
) {
# List of fields here
} @chrissm79 The additional configuration options you mentioned for defining transformers, validators or policies can be added in as well. |
Hello people, big feature here,,, Why not just make a Life should continue as it was,,, some people will use a controller some will use a repository,,, Laravels naming conventions should work for any class provided, otherwise will look into the repository. Keep it simple! |
Just a quick update on this, if anyone is following along. We made some big steps towards this with formal schema manipulation and are starting to make some necessary changes how Input Objects are handled. There are still a bunch of things that need to happen before this, but it is definitely still planned. |
Closing this as #207 implements basic nested mutations. |
I am currently trying to build an API on top of an existing Laravel backend, using laravel-graphql. Querying data seems to work fine, although the amount of boilerplate code is quite high.
However, i found myself struggling with implementing mutations.
We have 2 major requirements that make it quite hard:
Prisma does offer a sensible structure for how nested mutations can be dealt with and i feel it would play very nicely with Eloquent, have a look: https://www.prisma.io/docs/1.9/reference/prisma-api/mutations-ol0yuoz6go#nested-mutations
I think we might be able to deduce sensible defaults for CRUD operations simply by defining the types in the schema definition, what do you think?
The text was updated successfully, but these errors were encountered: