Editor writing to JSON REST API?

Editor writing to JSON REST API?

Rusty BallingerRusty Ballinger Posts: 21Questions: 7Answers: 0

I have existing REST APIs which take JSON objects, and they worked great for getting data for display DataTables; I was hoping insert/update/delete through Editor would be just as easy, but I'm not finding quite what I was hoping for.

I see instructions here for making Editor do (for example) a POST to /api/users to add a new user, PUT or POST to /api/users/666 to edit user 666, and DELETE to /api/users/666 to delete user 666 (and using separate methods like that is working for me), and I see instructions here for converting the request to JSON (which is also working for me), but... my problem is that the structure of the data is not quite what my existing APIs want. For example, if my User object looks like this:

{
  "id": 666,
  "name": "Necrobutcher",
  "jobTitle": "Bassist"
}

then a POST to /api/users expects one of those (minus the "id" attribute), and a PUT or POST to /api/users/666 expects one of those with the updated values (and, again, it's fine if the "id" attribute is absent, as I get it from the URL anyway). Instead, adding a new user sends this to /api/users:

{
  "action": "create",
  "data": {
    "0": {
      "name": "Euronymous",
      "jobTitle": "Guitarist"
    }
  }
}

and editing an existing user sends this to /api/users/666:

{
  "action": "edit",
  "data": {
    "666": {
      "name": "Necrobutcher",
      "jobTitle": "Survivor"
    }
  }
}

My options seem to be A) fiddle with the structure in JavaScript to match what my existing APIs expect, or B) add APIs on the server side to support the structure Editor wants.

One disadvantage of A is that I'm a Java guy, not so much a JavaScript guy, so the less JavaScript I write, the better. Copying the approach from here, it looks like I can extract the first value of the data hash and pass that as the created/edited object... and handle success & failure, as in the example... but that seems fragile, and I'm not sure I have the JavaScript chops to make that reusable (it'll need to be slightly different in a bunch of different pages).

The disadvantage of B (which, unlike A, I know I can do) is that I don't want to write new server code every time I add a new client! And if I'm doing that, the client-side code gets a lot simpler if I just do things the way Editor wants, and don't make it RESTful, instead having one endpoint per DataTable (rather than one for insert, one for edit, one for delete).

Thoughts? Is there an existing library which already does what I want? I know I'm not the first person trying to do this sort of thing!

This question has an accepted answers - jump to answer

Answers

  • Rusty BallingerRusty Ballinger Posts: 21Questions: 7Answers: 0

    The disadvantage of B (which, unlike A, I know I can do) is that I don't want to write new server code every time I add a new client!

    Biting the bullet and just doing it on the server really isn't too bad; I got inserts working in under an hour (probably 20 minutes of which was spent shaking my fist and shouting "why must I mess up my beautiful API?"), and updates & deletes will take less than that. Maybe I'll add my code here afterward.

  • allanallan Posts: 63,455Questions: 1Answers: 10,465 Site admin

    The ajax option provides the ability to send the different create, edit and remove commands to different URLs, along with different verbs (needed by REST) - example here.

    It also allows you to include the row id in the url - e.g.

    ajax: {
      edit: {
        url: "/api/users/_id_",
        type: 'PUT'
      },
      ...
    }
    

    Very likely you will want to use preSubmit to modify the data that Editor sends to the server since most REST APIs don't support multi-row editing (so there is no need to encapsulate the data in the data object).

    Allan

  • Rusty BallingerRusty Ballinger Posts: 21Questions: 7Answers: 0

    The ajax option provides the ability to send the different create, edit and remove commands to different URLs, along with different verbs (needed by REST) - example here.

    It also allows you to include the row id in the url - e.g.

    Yes, that's working for me (that's the first link in my question, and the row ID is included in the URL in my "editing an existing user" code snippet above); the issue is that the structure of the data being sent to the server isn't what the existing server-side API expects.

    It looks like using preSubmit would let me extract the element I want to send... but then the problem is that Editor wants to receive the new/updated object wrapped in other stuff too (and I think it doesn't like that I'm returning HTTP 400s instead of 200s on errors), so I found it easier to just accommodate Editor on the server side.

    (In a minute I'll post the server-side changes I made, in case anyone else might find them useful.)

  • Rusty BallingerRusty Ballinger Posts: 21Questions: 7Answers: 0

    Suppose you have an existing RESTful API using Spring, and the Java code for your clean, beautiful API looks like this:

    @RestController
    @RequestMapping("/api/yourThings")
    public class YourThingController {
    
        @Autowired
        private YourThingRepository yourThingRepository;
    
        @RequestMapping(method = GET)
        public List<YourThing> getYourThings(Principal principal) {
            //  ... check access
            return yourThingRepository.findAll();
        }
    
        @RequestMapping(value = "/{yourThingID}", method = GET)
        public YourThing getYourThing(Principal principal,
                @PathVariable int yourThingID) {
            //  ... check access
            return yourThingRepository.find(yourThingID);
        }
    
        @RequestMapping(method = POST)
        public ResponseEntity<YourThing> addYourThing(Principal principal,
                @RequestBody YourThing thing) {
            //  ... check access
            //  ... validate thing
            YourThing newThing = yourThingRepository.save(thing);
            HttpHeaders headers = new HttpHeaders();
            headers.setLocation(ServletUriComponentsBuilder
                    .fromCurrentRequest().path("/{yourThingID}")
                    .buildAndExpand(newThing.getThingID()).toUri());
            return new ResponseEntity<>(newThing, headers, HttpStatus.CREATED);
        }
    
        @RequestMapping(value = "/{yourThingID}", method = POST)
        public ResponseEntity<YourThing> editYourThing(Principal principal,
                @PathVariable int yourThingID,
                @RequestBody YourThing thing) {
            //  ... check access
            YourThing existing = yourThingRepository.findOne(yourThingID);
            //  ... validate thing, copy changes into existing
            yourThingRepository.save(existing);
            return new ResponseEntity<>(existing, null, HttpStatus.OK);
        }
    
        @RequestMapping(value = "/{yourThingID}", method = DELETE)
        @ResponseStatus(HttpStatus.NO_CONTENT)
        public void deleteYourThing(Principal principal,
                @PathVariable int yourThingID) {
            //  ... check access
            yourThingRepository.delete(yourThingID);
        }
    }
    

    (Note that, in that example, YourThing uses int IDs, but it could use String or whatever instead.)

    Because you might have many classes which have their own controllers which will be edited through Editor, you're going to add a couple of general-purpose classes to handle communication with Editor.

    /**
     * This is the structure we get from the jQuery DataTables Editor plugin when
     * the user is inserting/updating/deleting things.
     * 
     * @param <K> the type of key used by the class being wrapped.
     * @param <V> the class being wrapped.
     */
    public class EditorRequest<K, V> {
    
        private String action;
        private Map<K, V> data;
    
        //  ... have your IDE generate getters & setters; plus:
    
        public boolean isInsert() {
            return "create".equals(action);
        }
        public boolean isUpdate() {
            return "edit".equals(action);
        }
        public boolean isDelete() {
            return "remove".equals(action);
        }
    }    
    

    And this class:

    /**
     * This is the structure we send back to the jQuery DataTables Editor plugin
     * when the user is inserting/updating/deleting things.
     *
     * @param <T> the class being wrapped.
     */
    public class EditorResponse<T> {
        public static class FieldError {
            String name;
            String status;
            public FieldError(String name, String status) {
                this.name = name;
                this.status = status;
            }
        }
        List<T> data;
        String error;
        List<FieldError> fieldErrors;
    
        //  ... have your IDE generate getters & setters; plus:
    
        public void add(T newData) {
            if (data == null) data = new ArrayList<T>();
            data.add(newData);
        }
    
        public void addFieldError(String fieldID, String message) {
            if (fieldErrors == null) fieldErrors = new ArrayList<FieldError>();
            fieldErrors.add(new FieldError(fieldID, message));
        }
    }
    

    Then, back in your controller, add this method. Note that you need to make sure the request mapping won't collide with one of your objects' IDs; if that's a concern, see later.

        /**
         * This is the method which handles writes from DataTable.Editor.  Not
         * really RESTful.
         */
        @RequestMapping(value = "/editor", method = POST)
        public ResponseEntity<EditorResponse<YourThing>> handleEditor(
                Principal principal,
                @RequestBody EditorRequest<Integer, YourThing> request) {
            EditorResponse<YourThing> response = new EditorResponse<>();
            try {
                for (Integer id : request.getData().keySet()) {
                    if (request.isInsert()) {
                        ResponseEntity<YourThing> re = addYourThing(principal,
                                request.getData().get(id));
                        response.add(re.getBody());
                    } else if (request.isUpdate()) {
                        ResponseEntity<YourThing> re = editYourThing(principal,
                                id, request.getData().get(id));
                        response.add(re.getBody());
                    } else if (request.isDelete()) {
                        deleteYourThing(principal, id);
                    } else {
                        throw new SomeServiceArgumentException(
                                "Unhandled action \"" + req.getAction() + "\"");
                    }
                }
            } catch (SomeServiceArgumentException ex) {
                response.setError(ex.getMessage());
            }
            return new ResponseEntity<>(response, null, HttpStatus.OK);
        }
    

    Or, if you don't want to mess with your controller (because you don't want the editor URL to collide with one of your YourThing URLs), you could add a separate controller, @Autowire your YourThingController, and choose a request mapping which isn't under /api/yourThings.

    Back on the JavaScript side, convert to JSON on the way in:

        editor = new $.fn.dataTable.Editor({
                ajax: {
                  url: '/api/yourThings/editor',  // or whatever
                  contentType: 'application/json',
                  data: function ( d ) {
                    return JSON.stringify( d );
                  }
                },
                ...
    

    Hopefully that saves someone some time!

  • allanallan Posts: 63,455Questions: 1Answers: 10,465 Site admin
    Answer ✓

    Amazing - thank you for writing that up!

    Allan

  • ZhenZhen Posts: 2Questions: 1Answers: 0

    Chrome's developer tools shows that Editor's ajax call always converts any contenttype to "www-form-urlencoded" when sending a request to the server
    no matter what is manually specified in Options 'contentType' and "dataType".
    On the other hand, FormHttpMessageConverter in RestController has no ability to understand and map a "complicated-structure" request of contenttype "www-form-urlencoded" to a MultiValueMap object.
    This is a key problem in Editor's ajax call to Java server side.

  • colinmackenziecolinmackenzie Posts: 1Questions: 0Answers: 0

    Hi Folks,
    Has anyone used this for C# ?
    Being an Oracle developer with little C# skills I need to try and mimic this solution.
    I think it's more the bit about adding the getters and setters that's confusing me.
    Any help would be much appreciated in trying to get a handle on the request and response objects.
    Cheers

This discussion has been closed.