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

Rendering Domain Models #81

Open
DeweertD opened this issue Nov 1, 2024 · 5 comments
Open

Rendering Domain Models #81

DeweertD opened this issue Nov 1, 2024 · 5 comments

Comments

@DeweertD
Copy link

DeweertD commented Nov 1, 2024

I am running this plugin using Grails 6.2.1, Vue, Gorm-hibernate, etc. Mostly just the standard setup

Hello, sorry to bother you, but I was wondering if you have any idea how to render domain models using inertia? Any domain model with associations ends up throwing a StackOverflow for me.

I'm using a recursive domain model in this case, but I don't think the Json renderer is supposed to follow proxies, which should make it safe to render. I know on just a simple test output I can render to json using "as JSON", but your plugin doesn't seem to support this use case.

`
Caused by: grails.views.ViewException: Error rendering view: null
at grails.views.AbstractWritableScript.writeTo(AbstractWritableScript.groovy:46)
at grails.plugin.inertia.Inertia.renderHtml(Inertia.groovy:113)
at grails.plugin.inertia.Inertia.renderInternal(Inertia.groovy:91)
at grails.plugin.inertia.Inertia.render(Inertia.groovy:67)
at grails.plugin.inertia.InertiaTrait$Trait$Helper.renderInertia(InertiaTrait.groovy:34)
at com.myproject.HomeController.index(HomeController.groovy:11)
... 83 common frames omitted
Caused by: java.lang.StackOverflowError: null
at grails.plugin.json.converters.InstantJsonConverter.handles(InstantJsonConverter.groovy:18)
at grails.plugin.json.builder.DefaultJsonGenerator.findConverter(DefaultJsonGenerator.java:450)
at grails.plugin.json.builder.DefaultJsonGenerator.writeObject(DefaultJsonGenerator.java:194)
at grails.plugin.json.builder.DefaultJsonGenerator.writeMapEntry(DefaultJsonGenerator.java:412)
at grails.plugin.json.builder.DefaultJsonGenerator.writeMap(DefaultJsonGenerator.java:396)
at grails.plugin.json.builder.DefaultJsonGenerator.writeObject(DefaultJsonGenerator.java:242)

`

@matrei
Copy link
Owner

matrei commented Nov 1, 2024

Hi @DeweertD, how are you calling renderInertia in your HomeController

@DeweertD
Copy link
Author

DeweertD commented Nov 1, 2024

Thanks you so much for the quick reply!

I'm not doing anything too crazy, but I do have a well-connected dataset. I'm modelling a filesystem to allow for a "playground" where users can re-map their existing files/folders to a new set of files/folders. Once they are happy they can tell a service to run the job, but in the meantime all changes are just persisted in the database without affecting the filesystem.

For some reason, when rendering json through Inertia, the json builder doesn't care that it's a domain class, so all properties and relationships get serialized recursively.
Properties like metaClass, isDirty, gorm_hibernate_.... AST properties, and even the parent<-> child loop (however I tested without that loop and it was still overflowing just by traversing one direction)

@Secured(['ROLE_ADMIN'])
class HomeController {

    def index() {
        def folders = Folder.where{parent == null}.join("children").join("files").join("children.files").get()
        renderInertia('Home', [folders: folders])
    }
}

The above is intended to be slightly greedy by providing two layers of folders initially, then the client can request more as users interact with the UI.

Note: withFormat { json { respond folders } } worked as intended (no UI obviously, but the browser received exactly the data I was retrieving)

@DeweertD
Copy link
Author

Hi @matrei, any chance you were able to look at this?

I have figured out that writing a JsonConverter helped with the inertia rendering specifically, however I also needed to separately write some .gson files whenever I was sending raw JSON responses for axios requests. This is a duplication of effort I would like to avoid, however I'm now sure how that could be accomplished since it seems inertia responses need to render json in an string to inject in the html.gsp page, meanwhile raw json responses are a separate response type with their own rendering pipeline. (specifically they use .gson files in the grails-app/views resource package

// /src/main/groovy/com/example/converters/FolderJsonConverter.groovy
package com.example.converters

import com.example.Folder
import grails.plugin.json.builder.JsonGenerator
import org.grails.orm.hibernate.cfg.GrailsHibernateUtil

class FolderJsonConverter implements JsonGenerator.Converter
{


    @Override
    boolean handles(Class<?> type)
    {
        Folder.isAssignableFrom(type)
    }


    @Override
    Object convert(Object value, String key)
    {
        Folder folder = (Folder) value

        def children = GrailsHibernateUtil.isInitialized(folder, "children")
                ? folder.children
                : null

        def files = GrailsHibernateUtil.isInitialized(folder, "files")
                ? folder.files
                : null


        def folder_map = [id              : folder.id,
                          name            : folder.name,
                          children        : children,
                          files           : files,
        ]

        if (GrailsHibernateUtil.isInitialized(folder, "parent") && folder.parent != null)
        {
            folder_map['parent'] = [id: folder.parent.id]
        }

        return folder_map
    }
}

Combined with the configuration to register the above converter

// src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter
com.example.converters.FolderJsonConverter
com.example.converters.FileJsonConverter
com.example.converters.GrailsUserJsonConverter
com.example.converters.UserJsonConverter
com.example.converters.RoleJsonConverter

And the corresponding json views to render raw JSON when doing axios requests from the front end

// /grails-app/views/folder/_folder.gson
import com.example.Folder

model {
    Folder folder
}
json {
    children g.render(folder.children) // render default instead of template to not stack overflow
    files tmpl.'/file/file'(folder.files)
    name folder.name
    id folder.id
}

@matrei
Copy link
Owner

matrei commented Nov 28, 2024

Hi @DeweertD, sorry I have not had the time to get back to this yet.

Have you had a look at how I solved this in https://github.com/matrei/pingcrm-grails with the @PublicData trait and @PublicDataMapper class?

@DeweertD
Copy link
Author

DeweertD commented Jan 4, 2025

Hello @matrei, sorry about the extra long delay. I spent a little time looking into this issue finally, and I came up with what seems to be a fairly good solution, though I don't know if it has larger ripples concerning the custom data marshalling you do in the example pingcrm-grails project.

Here is the commit, as you can see it's really simple, and it seems to remove my issue of stack overflow / duplicate serialization logic in the JsonConverters. By implementing this simple change I was able to finally let the inertia plugin directly utilize the gson templates I had written for the rest of the project.

Let me know your thoughts, questions, or concerns. I'd be happy to open an MR for this too if you think it's worth incorporating?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants