Sending array objects in JSDO back to OpenEdge Rest Server

Posted by alextcl on 03-Dec-2014 03:39

Hi all, 

I am working on a mobile app and collaborating with another OpenEdge developer for his REST service data model design.

The type of Mobile App is  OpenEdge Mobile.

One of the functionality of the app is to allow user to provide respond for questions that will be displayed in each page individually.
It could be a multiple choice, numeric or character input answers. The app will store the answers in form of stringify JSON data to the local storage variable. It will then send the answers back to the server once the user confirm the submission.

The sample data that I stored locally is as below:

{

    "TaskNo": "45687",

    "DateTime": "Mon, 14 Jul 2014 07:00:00 GMT",

    "Latitude": 53.48137,

    "Longitude": -2.23174,

    "Answers": [

        {

            "QuestionId": "WA210",

            "Type": "R",

            "StaticDynamic": "Dynamic",

            "ResponseValue": "Yes",

            "IssueSeverity": null,

            "Comments": null,

            "PhotoImage": null

        },

        {

            "QuestionId": "WA211",

            "Type": "I",

            "StaticDynamic": "Dynamic",

            "Answers": "10",

            "IssueSeverity ": null,

            "Comments": null,

            "PhotoImage": null

        },

        {

            "QuestionId": "WA212",

            "Type": "I",

            "StaticDynamic": "Dynamic",

            "ResponseValue ": "10",

            "IssueSeverity ": ‘Mandatory’,

            "Comments": "Mandatory",

            "PhotoImage": [

                "image encoded in base64 /photopath1.jpg"

            ]

        },

        {

            "QuestionId": "WA212",

            "Type": "I",

            "StaticDynamic": "Static",

            "ResponseValue": "10",

            "IssueSeverity": null,

            "Comments": "Write a really long message here",

            "PhotoImage": [

                "image encoded in base64 /photopath1.jpg",

                "image encoded in base64 /photopath1.jpg"

            ]

        }

    ]

}

And the table definition of OpenEdge JSDO service for the answer would be similar to the following:

DEFINE TEMP-TABLE ttCheckQuestions no-undo

FIELD TaskNo        AS INTEGER

FIELD ConfDateTime  AS DATETIME-TZ

FIELD LegNo         AS INTEGER

FIELD DisplaySeq    AS INTEGER

FIELD VehicleType   AS CHARACTER

FIELD StaticDynamic AS CHARACTER

FIELD QuestionID    AS CHARACTER

FIELD QuestionText AS CHARACTER

FIELD QuestionHelp  AS CHARACTER

FIELD QuestionType  AS CHARACTER

FIELD ValidValues   AS CHARACTER

FIELD ResponseValue AS CHARACTER

FIELD IssueSeverity AS CHARACTER

FIELD Comments      AS CHARACTER

FIELD Longitude     AS CHARACTER

FIELD Latitude      AS CHARACTER

FIELD PhotoImage    AS BLOB

      INDEX KEY-1

            DisplaySeq.

 

However, the JSDO service for Create method generated by the OpenEdge server will only create a single record and the request parameters only dedicated for one object creation.

In this case, we couldn’t utilize the data source mapping features with Rollbase Mobile App designer.


I could think of multiple solutions to this problem, as listed below; but I don’t know if they are possible because I have no experience with OpenEdge development. 

  1. Define a JSDO method at OpenEdge server that will accept and parse my data format, which then create multiple line of record at the database by writing some Progress Code.
  2. Loop and call the JSDO create method for each answer object that I wanted to send back to the server ( Anoyone could share how to call the JSDO service without using the data mapping features of Rollbase App Designer? ).


Could anyone please advise us on this matter?

Thank you.

All Replies

Posted by Ricardo Perdigao on 03-Dec-2014 09:33

Alex,

Both options you provided would work. You can send the JSON string to the backend and parse it on 4GL to create each record.  Or, you can loop on the Mobile App and call the JSDO manually to send the data as a single record each time.  The second option would need some considerable knowledge of the JSDO api and JavaScript programming.

Option 1 is very simple and you would pass a Character field with the stringfy from the LocalStorage variable.  The problem here might be how big that string can get. Maybe place each answer JSON string in a separate field?

Option 2 is more complex, but you don't have to worry about the string going beyond what the character field can accommodate.  The complex piece is to build the JSDO commands, but there are good documentation on using the JSDO api at Communities and Manuals.  If you get stuck building the syntax, post how you are trying to write it in the communities and I am positive someone will help you.

All the best,

Ricardo

Posted by egarcia on 03-Dec-2014 09:39

Hello,

Just some suggestions.

For your approach mentioned in 1.

You can call the JSDO API directly by using the "jsdo" handle from the JSDO Service:

Example:

  MobilityDemoService_dsCustomer_JSDO.jsdo.sort(["CustNum"]);

with the JSDO reference, you can call an INVOKE method in the Business by just using the name of the method.

  MobilityDemoService_dsCustomer_JSDO.jsdo.myMethod( Object-with-parameters-DataSet-or-TempTable );

You would need to transform the data that you showed above to use the format of DataSet or a Temp-Table.

Alternatively, you can just send the JSON object as a string and then process it in the backend using the JSON API in the ABL. The advantage of using the object with parameters is that the processing of the multiple records in the array then would be handled by the ABL instead of you having to parse it with the JSON API.

For your approach mentioned in 2.

Using the JSDO reference, you can call the add() method.

  MobilityDemoService_dsCustomer_JSDO.jsdo.myMethod( object-with-fields );

Then call saveChanges():

  MobilityDemoService_dsCustomer_JSDO.jsdo.saveChanges();

Please notice that the invoke method and saveChanges() are by default asynchronous. You would need to define a call back to process the response from the server.

If you are using OpenEdge 11.4, you can use the JSDO Submit service which will send a DataSet with all the new records. (It internally calls saveChanges().)

An alternative to using the add() method is to use addRecords() to populate the JSDO memory with the values, however, this is a direct change and the records are not marked as new/modified so you would have to loop through them to modify them so that they are marked as modified. (Then you would call saveChanges() or the JSDO Submit service.)

The following example uses addRecords():

oemobiledemo.progress.com/.../example006.html

You can use the Web Inspector to look at the source code.

I hope this helps.

Posted by spserna2 on 08-Dec-2014 10:51

Thanks Richardo and egarcia,

I have followed the 2nd option since it will be easier for OpenEdge developer.

I trying to wrap my JSDO call in a Generic Service, and MCBDriversService_BECheckQuestions_JSDO is the JSON catalogue I imported from OpenEdge.

$t.SubmitChecklist = $t.createClass(null, { 

    init: function(requestOptions) {

        this.__requestOptions = $.extend({}, requestOptions);

    },

    process: function(settings) {

        if (this.__requestOptions.echo) {

            settings.success(this.__requestOptions.echo);

        } else {

            console.log('This will start the checklist submission process');

           

            var taskNo = settings.data.taskNo;

            var longitude = settings.data.longitude;

            var latitude = settings.data.latitude;

            var dateTime = new Date(settings.data.dateTime);

            var questionCount = parseInt(settings.data.questionCount);

            console.log('Setting values are: ' + taskNo + '_' + questionCount + '_' + longitude + '_' + latitude + '_' + dateTime);

           

            var allAnswers = [];

            var questionName = 'question';

            var answerName = 'answer';

           

            var svc_jsdo = MCBDriversService_BECheckQuestions_JSDO.jsdo;

            console.log(svc_jsdo);

            for (var i = 0; i < questionCount; i++) {

                var questionData = JSON.parse(localStorage.getItem(questionName + i));

                var answerData = JSON.parse(localStorage.getItem(answerName + i));

               

                questionData.ResponseValue = answerData.ResponseValue;

                questionData.IssueSeverity = answerData.IssueSeverity;

                questionData.Comments = answerData.Comments;

                questionData.PhotoImage = answerData.PhotoImage1;

                questionData.Longitude = longitude;

                questionData.Latitude = latitude;

                questionData.ConfDateTime = dateTime;

               

                console.log(questionData);

                svc_jsdo.add(questionData);

            }

            /* Before sending the request, save it away so we execute

             * only the function for this DataSource */

            var beforeSaveChangesFn = function(jsdo, request) {

                jsdo.unsubscribe('beforeSaveChanges', beforeSaveChangesFn);

                settings.request = request;

            };

           

            var afterSaveChangesFn = function(jsdo, success, request) {

                /* If not for the same request saved away on the before

                 * saveChanges fn, just return */

                if (request != settings.request) return;

               

                /* Unsubscribe so this fn doesn't execute for some other

                 * Tiggr.DataSource event */

                jsdo.unsubscribe('afterSaveChanges', afterSaveChangesFn);

               

                var cStatus = 'success';

                if (success || (request.xhr.status >= 200 && request.xhr.status < 300)) {

                    settings.success(request.response);

                } else {

                    var cError = normalizeError(request);

                   

                    settings.error(request.xhr, cError);

                    cStatus = cError;

                }

                settings.complete(request.xhr, cStatus);

            };

           

            svc_jsdo.subscribe('beforeSaveChanges', beforeSaveChangesFn);

            svc_jsdo.subscribe('afterSaveChanges', afterSaveChangesFn);

            svc_jsdo.saveChanges();

        }

    }

});


I had some error message related to ProDataSource.

{"_retVal":"ProDataSource extent value must be a valid DataSource Handle.","_errors":[{"_errorMsg":"ERROR condition: The Server application has returned an error. (7243) (7211)","_errorNum":0}]}

I google up a bit but there is no documentation describing the error, anyone know what is this error about,
for references, I exported my Network analysis information in .har file,

https://drive.google.com/file/d/0B3Nnv9mByE-pbHc1a3FRd09EcUE/view?usp=sharing

You could use https://toolbox.googleapps.com/apps/har_analyzer/ to analyze the data traffic with the server.

Posted by Thomas Mercer-Hursh on 08-Dec-2014 11:06

$t.SubmitChecklist = $t.createClass(null, { 
    init: function(requestOptions) {
        this.__requestOptions = $.extend({}, requestOptions);
    },
    process: function(settings) {
        if (this.__requestOptions.echo) {
            settings.success(this.__requestOptions.echo);
        } else {
            console.log('This will start the checklist submission process');
       
            var taskNo = settings.data.taskNo;
            var longitude = settings.data.longitude;
            var latitude = settings.data.latitude;
            var dateTime = new Date(settings.data.dateTime);
            var questionCount = parseInt(settings.data.questionCount);

            console.log('Setting values are: ' + taskNo + '_' + questionCount + '_' + longitude + '_' + latitude + '_' + dateTime);
       
            var allAnswers = [];
            var questionName = 'question';
            var answerName = 'answer';
       
            var svc_jsdo = MCBDriversService_BECheckQuestions_JSDO.jsdo;

            console.log(svc_jsdo);
            for (var i = 0; i < questionCount; i++) {
                var questionData = JSON.parse(localStorage.getItem(questionName + i));
                var answerData = JSON.parse(localStorage.getItem(answerName + i));
               
                questionData.ResponseValue = answerData.ResponseValue;
                questionData.IssueSeverity = answerData.IssueSeverity;
                questionData.Comments = answerData.Comments;
                questionData.PhotoImage = answerData.PhotoImage1;
                questionData.Longitude = longitude;
                questionData.Latitude = latitude;
                questionData.ConfDateTime = dateTime;
              
                console.log(questionData);
                svc_jsdo.add(questionData);
            }
            /* Before sending the request, save it away so we execute
             * only the function for this DataSource */
            var beforeSaveChangesFn = function(jsdo, request) {
                jsdo.unsubscribe('beforeSaveChanges', beforeSaveChangesFn);
                settings.request = request;
            };
        
            var afterSaveChangesFn = function(jsdo, success, request) {
                /* If not for the same request saved away on the before
                 * saveChanges fn, just return */
                if (request != settings.request) return;
               
                /* Unsubscribe so this fn doesn't execute for some other
                 * Tiggr.DataSource event */
                jsdo.unsubscribe('afterSaveChanges', afterSaveChangesFn);
               
                var cStatus = 'success';
                if (success || (request.xhr.status >= 200 && request.xhr.status < 300)) {
                    settings.success(request.response);
                } else {
                    var cError = normalizeError(request);
             
                    settings.error(request.xhr, cError);
                    cStatus = cError;
                }
                settings.complete(request.xhr, cStatus);
            };
           
            svc_jsdo.subscribe('beforeSaveChanges', beforeSaveChangesFn);
            svc_jsdo.subscribe('afterSaveChanges', afterSaveChangesFn);
            svc_jsdo.saveChanges();
        }
    }
});

There is lots to be said for using the code insert at the right of the second line of icons under Formatted

Posted by egarcia on 08-Dec-2014 12:42

Hello,

>> ProDataSource extent value must be a valid DataSource Handle.

The error message that you are getting is thrown by the abstract Business Entity in 11.4 that is used in generated Business Entities.

The error message is thrown when the hDataSourceArray variable has not been initialized.

This could happen with Business Entities created from a schema file. This issue would not happen if you create the Business Entity from a database table.

To fix this issue, you need to change the following code:

    /* TODO Fill in with appropriate data-sources */

    /* hDataSourceArray[1] =  DATA-SOURCE src<db-table>:HANDLE. */

To look like the following:

       hDataSourceArray[1] =  DATA-SOURCE srcCustomer:HANDLE.

Once you correct this issue, the generic service that you wrote should work.

As mentioned, your generic service seems to be fine. However, another alternative, is to change use the Submit service part of the Mobile App Builder project.

For this, you would create the Business Entity from a database table using te CRUD + Submit option.

The generated code will then use a DataSet which is needed to handle before-image information.

(From the Network activity that you posted, it looks like your Business Entity is for a temp-table and not for a DataSet.)

Then you would just need to process the values in localStorage and then call the Submit service. (Internally, the code would behave very similar to your generic service and use saveChanges(true).)

(The code to process the values in localStorage would most likely live in a JavaScript function instead of the generic service.)

I hope this helps.

Posted by spserna2 on 07-Jan-2015 11:47

thanks egarcia,

I actually has little control over the server side code mainly because it was develop by external developer.

I tried to use saveChanges(true) but it seen that Submit method is not defined on the server. Is the developer has to explicitly define the Submit method function on the ABL level?

At the moment, we proceed with my current code. I found that it is using POST and I assume that the CREATE method of Business Entity would be called. Is there a way to convert the call to use Update (PUT) instead of POST?

I have not receiving the complete event from my generic service and I believe the problem would be following lines

            /* Before sending the request, save it away so we execute
             * only the function for this DataSource */
            var beforeSaveChangesFn = function(jsdo, request) {
                jsdo.unsubscribe('beforeSaveChanges', beforeSaveChangesFn);
                settings.request = request;
            };
            
            var afterSaveChangesFn = function(jsdo, success, request) {
                /* If not for the same request saved away on the before
                 * saveChanges fn, just return */
                if (request != settings.request) return;

https://drive.google.com/file/d/0B24xbK5oVbwKdXlZU1NqeGl5TXc/view?usp=sharing

In my Network activity (see .har link above), I noticed that it spawn a POST request for every objects that I added. In this case, I should have not receive the original request from beforeSaveChanges event...

Is the correct method to track the completion of the save would be waiting all the POST request to completed ?

There is an example from OpenEdge documentation...
http://documentation.progress.com/output/ua/OpenEdge_latest/index.html#page/dvmad/aftersavechanges-event.html#

function onAfterSaveChanges( jsdo ,  success , request ) {
    
    /* number of operations on batch */
    var len = request.batch.operations.length;
    
    if (success) {

        /* all operations in batch succeeded */
        /* for example, redisplay records in list */
        jsdo.foreach( function(jsrecord) {
            /* reference the record/field as jsrecord.data.fieldName */
        });

    }
    else {
        /* one or more operations in batch failed */
        for(var idx = 0; idx < len; idx++) {

            var operationEntry = request.batch.operations[idx];
    
            console.log("Operation: " + operationEntry.fnName);
            console.log("Operation code: " + operationEntry.operation)
    
            if (!operationEntry.success) {

              /* handle error condition */
              if (operationEntry.response && operationEntry.response._errors && 
                    operationEntry.response._errors.length > 0) {

                    var lenErrors = operationEntry.response._errors.length;
                    for (var idxError=0; idxError < lenErrors; idxError++) {

                        var errors = operation.response._errors[idxError];
                        var errorMsg = errors._errorMsg;
                        var errorNum = errors._errorNum;
                        /* handle error */

                    }
                }
            }
            else {
                /* operation succedeed */
            }
        }        
    }
};

I assume that if I could get the SUBMIT function work, I could save a a lot of problems..

Posted by spserna2 on 08-Jan-2015 11:42

An update to this post,

we had solved the problem and use the SUBMIT method for this scenario and it save significant amount of network round trips.

In fact, more works has to be done on the OpenEdge server compared to the client side. In order to use the SUBMIT method, Select the CRUD + Submit option when creating the Business Entity.

Ensure that the temp-table declaration has the before table name

DEFINE TEMP-TABLE ttCheckQuestions NO-UNDO BEFORE-TABLE bttCheckQuestions

Just compile the code and the catalogue files will contains the JSDO submit methods

 "operations": [
                    {
                        "path": "",
                        "useBeforeImage": false,
                        "type": "update",
                        "verb": "put",
                        "params": [{
                            "name": "dsCheckQuestions",
                            "type": "REQUEST_BODY"
                        }]
                    },
                    {
                        "name": "SubmitBECheckQuestions",
                        "path": "\/SubmitBECheckQuestions",
                        "useBeforeImage": true,
                        "type": "submit",
                        "verb": "put",
                        "params": [{
                            "name": "dsCheckQuestions",
                            "type": "REQUEST_BODY"
                        }]
                    },

On the client side, 
just perform the normal add operation and then call saveChanges(true) as told by egarcia.

It will be a PUT request to the server. (see attachment)

However, I actually want to update the data instead of creating new one. I am still could not figure the proper way to tell the dataset my data is updated data rather than new data.

Posted by egarcia on 09-Jan-2015 05:49

Hello,

Just a quick reply.

Depending on your requirements, you could do the following:

1) Send corresponding records from the server for the fill() / READ operation and use assign() to update the records. In this case, the operations sent to submit (via saveChanges(true)) would be considered updates.

2) You could also change the code in the Business Entity to handle the records as required. Notice that the Business Entity does not have to match exactly your database tables, we could say that the Business Entity is like a view to the data which executes your business logic. For example, in your current configuration you could change the code for SubmitBECheckQuestions to process the added records as if they were updates:

   METHOD PUBLIC VOID SubmitBECheckQuestions(INPUT-OUTPUT DATASET dsCheckQuestions):          

    DEFINE VAR hDataSet AS HANDLE NO-UNDO.

    hDataSet = DATASET dsCheckQuestions:HANDLE.

    FOR EACH ttCheckQuestions WHERE ROW-STATE(ttCheckQuestions) = ROW-CREATED:

           /* Process request */

       END.

      /* SUPER:Submit(DATASET-HANDLE hDataSet BY-REFERENCE). */

   END METHOD.

Do you do anything with the records on the client after the Submit operation?

The Submit operation as well as any other operation in the Business Entity returns the changes to the data on the server.

For example, if you were creating a new record and the value of one of the fields comes from a sequence in a database trigger, the value would be returned to client in the HTTP response.

This behavior happens by default. However, if you were to use option 2 above, you would need to make sure that this behavior is preserved (if you depend on it).

Another alternative, would be to create a method in the Business Entity to process a set of records and call it as an INVOKE operation from the client.

I hope this helps.

This thread is closed