Render metadata for pagination from Grails RestfulController index/search actions

grails, json, metadata, pagination, rest

Solution

Based on ideas from

http://mrhaki.blogspot.com/2013/12/grails-goodness-rendering-partial.html

http://groovyc.net/non-trivial-restful-apis-in-grails-part-2/

- Creat a custom CollectionRenderer e.g., SumoJsonCollectionRenderer.groovy

import grails.converters.JSON
import grails.rest.render.RenderContext

import grails.rest.render.json.JsonCollectionRenderer
import org.codehaus.groovy.grails.web.mime.MimeType

import groovy.transform.CompileStatic
import static groovy.transform.TypeCheckingMode.SKIP

@CompileStatic
class SumoJsonCollectionRenderer extends JsonCollectionRenderer{

    SumoJsonCollectionRenderer(Class componentType) {
        super(componentType)
    }

    public SumoJsonCollectionRenderer(Class componentType, MimeType... mimeTypes) {
        super(componentType, mimeTypes)
    }

    @CompileStatic(SKIP)
    @Override
    protected void renderJson(object, RenderContext context) {
        log.debug(object)
        log.debug(object.size())
        log.debug(object.getTotalCount())
        Map tObject = ['results':object]
        if(context.arguments?.metadata) {
            tObject['metadata'] = context.arguments.metadata
        }
        super.renderJson(tObject,context)
    }
}

register custom CollectionRenderer at resource.groovy

for (domainClass in grailsApplication.domainClasses) {
    "json${domainClass.shortName}CollectionRenderer"(SumoJsonCollectionRenderer, domainClass.clazz)
    "json${domainClass.shortName}Renderer"(JsonRenderer, domainClass.clazz)
    "hal${domainClass.shortName}CollectionRenderer"(HalJsonCollectionRenderer, domainClass.clazz)
    "hal${domainClass.shortName}Renderer"(HalJsonRenderer, domainClass.clazz)
}

Request/Response

http://api.mydomain.com:8080/ApiApp/drugs.json?max=5&fields=ndc,id

{"results":[{"id":1,"ndc":"000020803031"},{"id":2,"ndc":"000021200011"},{"id":3,"ndc":"000021407011"},{"id":4,"ndc":"000021975901"},{"id":5,"ndc":"000023004751"}],"metadata":{"total":851,"psize":5,"offset":0}}

Problem

I am looking for best practices / solution to make 'respond' method generate extra metadata in the resulting json along with the collection of entities got from DB. Basically I wanted to implement pagination using that metadata in my frontend single-page-application (SPA) built with angularJS and Restangular plugin. PS: angularJS's $resource or Restangular expect collection results as JS array. Standard Grails `JsonCollectionRenderer/JsonRenderer` ignores the metadata supplied to 'respond' in the map argument. I came across following article which is implementing custom JsonRenderer, but I looking for simpler/flexible solution to make 'respond' output metadata via tweaking custom `JsonCollectionRenderer` in `resources.groovy` http://groovyc.net/non-trivial-restful-apis-in-grails-part-2/ My RestfulController: ``` @Secured(value=["hasRole('ROLE_USER')"]) class DrugController extends RestfulController<Drug> { static scaffold = true static responseFormats = ['html', 'json', 'xml', 'hal'] static allowedMethods = [show: "GET"] DrugController() { super(Drug, true) } @Override def index(Integer max) { params.max = Math.min(max ?: 10, 100) // We pass which fields to be rendered with the includes attributes, // we exclude the class property for all responses. ***when includes are defined excludes are ignored. //params.fetch = [recordTypeRs:"eager"] from params.fields??? respond resource.list(params), [includes: includeFields, excludes: ['class', 'errors', 'version'], metadata: [total: countResources(), psize: params.max, offset: params.offset?:0], model: [("${resourceName}InstanceCount".toString()): countResources()]] } @Override def show(Drug drug) { JSON.use("deep") { respond drug, [includes: includeFields, excludes: ['class', 'errors', 'version']] } } private getIncludeFields() { params.fields?.tokenize(',') } def search(Integer max) { params.max = Math.min(max ?: 10, 100) def c = Drug.createCriteria() def results = c.list(params) { //Your criteria here with params.q and { like('ndc', params.ndc?params.ndc+'%':'%') like('recordTypeJ.j017', params.labelerName?'%'+params.labelerName+'%':'%') like('recordTypeE.e017', params.productName?'%'+params.productName+'%':'%') } //cache(true) } log.debug(results.totalCount) respond results, model:[drugCount: results.totalCount] } ``` } I have following in my resources.groovy. ``` // register Renderers/CollectionRenderers for all domain classes in the application. for (domainClass in grailsApplication.domainClasses) { "json${domainClass.shortName}CollectionRenderer"(JsonCollectionRenderer, domainClass.clazz) "json${domainClass.shortName}Renderer"(JsonRenderer, domainClass.clazz) "hal${domainClass.shortName}CollectionRenderer"(HalJsonCollectionRenderer, domainClass.clazz) "hal${domainClass.shortName}Renderer"(HalJsonRenderer, domainClass.clazz) } ```

Original source