Following is a sample Business Entity showing the method generated to implement the Data Object Read operation, with manual annotation and code changes required both to implement a JSDO for access by the Kendo UI DataSource and to implement a Rollbase external object.

This is the include file (customer.i) that is referenced by the Business Entity, including a ProDataSet (dsCustomer) that contains a single temp-table (ttCustomer), with fields that you add to the fields that correspond to the existing database table fields indicated in bold, and an additional index you must also add shown in bold:

DEFINE TEMP-TABLE ttCustomer BEFORE-TABLE bttCustomer
FIELD id            AS CHARACTER
FIELD seq           AS INTEGER      INITIAL ?
FIELD CustNum       AS INTEGER      INITIAL "0" LABEL "Cust Num"
FIELD Name          AS CHARACTER    LABEL "Name"
FIELD Address       AS CHARACTER    LABEL "Address"
FIELD Address2      AS CHARACTER    LABEL "Address2"
FIELD Balance       AS DECIMAL      INITIAL "0" LABEL "Balance"
FIELD City          AS CHARACTER    LABEL "City"
FIELD Comments      AS CHARACTER    LABEL "Comments"
FIELD Contact       AS CHARACTER    LABEL "Contact"
FIELD Country       AS CHARACTER    INITIAL "USA" LABEL "Country"
FIELD CreditLimit   AS DECIMAL      INITIAL "1500" LABEL "Credit Limit"
FIELD Discount      AS INTEGER      INITIAL "0" LABEL "Discount"
FIELD EmailAddress  AS CHARACTER    LABEL "Email"
FIELD Fax           AS CHARACTER    LABEL "Fax"
FIELD Phone         AS CHARACTER    LABEL "Phone"
FIELD PostalCode    AS CHARACTER    LABEL "Postal Code"
FIELD SalesRep      AS CHARACTER    LABEL "Sales Rep"
FIELD State         AS CHARACTER    LABEL "State"
FIELD Terms         AS CHARACTER    INITIAL "Net30" LABEL "Terms"
INDEX seq IS PRIMARY UNIQUE seq
INDEX CustNum IS UNIQUE CustNum
.

DEFINE DATASET dsCustomer FOR ttCustomer.
Note: For access by a Rollbase external object, you must implement the Business Entity to provide its data as a ProDataSet with only a single temp-table, as shown in this example. For access by Kendo UI, you can implement the Business Entity to provide its data either as a single temp-table or as a ProDataSet with one or more temp-tables.

The id field is added to each temp-table record to support Rollbase external objects.

The seq field is used to guarantee the order of records in the serialized temp-table that is returned as JSON to the JSDO. To work properly, this field must be initialized with the Unknown value (?). You must also add an index on seq that is both PRIMARY and UNIQUE. You can also have additional indexes, which can be the same or different than those in the database, as shown for CustNum, but the index on seq must be the PRIMARY one.

Following is the class file for the Business Entity, Customer.cls. Manually added annotations and code are in bold, except in the case of added methods, where only the first and last lines of the method are in bold:

@program FILE(name="Customer.cls", module="AppServer").
@openapi.openedge.export FILE(type="REST", executionMode="singleton", useReturnValue="false", writeDataSetBeforeImage="false").
@progress.service.resource FILE(name="Customer", URI="/Customer", schemaName="dsCustomer", schemaFile="Customer/AppServer/customer.i").

USING Progress.Lang.*.

USING OpenEdge.BusinessLogic.BusinessEntity.
USING Progress.Json.ObjectModel.*.

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS Customer INHERITS BusinessEntity:

    {"customer.i"}
    
    DEFINE DATA-SOURCE srcCustomer FOR Customer.
  
    DEFINE VARIABLE iSeq            AS INTEGER      NO-UNDO.

    CONSTRUCTOR PUBLIC Customer():
        
        DEFINE VAR hDataSourceArray AS HANDLE NO-UNDO EXTENT 1.
        DEFINE VAR cSkipListArray AS CHAR NO-UNDO EXTENT 1.
        
        SUPER (DATASET dsCustomer:HANDLE).
        
        /* Data Source for each table in dataset. 
           Should be in table order as defined in DataSet */
        hDataSourceArray[1] =  DATA-SOURCE srcCustomer:HANDLE.

        /* Skip-list entry array for each table in DataSet.
           Should be in temp-table order as defined in DataSet */
        /* Each skip-list entry is a comma-separated list of field names
           to be ignored in the CREATE statement */

        cSkipListArray[1] = "CustNum".

        THIS-OBJECT:ProDataSource = hDataSourceArray.
        THIS-OBJECT:SkipList = cSkipListArray.

    END CONSTRUCTOR.

    @openapi.openedge.export(type="REST", useReturnValue="false", writeDataSetBeforeImage="true").
    @progress.service.resourceMapping(type="REST", operation="read", URI="?filter=~{filter~}", alias="", mediaType="application/json"). 
    @openapi.openedge.method.property (name="mappingType", value="JFP").
    @openapi.openedge.method.property (name="capabilities", value="ablFilter,top,skip,id,orderBy").
    METHOD PUBLIC VOID ReadCustomer(
            INPUT filter AS CHARACTER, 
            OUTPUT DATASET dsCustomer):

      IF filter BEGINS "~{" THEN
        THIS-OBJECT:JFPFillMethod (INPUT filter).
      ELSE DO:
        BUFFER ttCustomer:HANDLE:BATCH-SIZE = 0.
        BUFFER ttCustomer:SET-CALLBACK ("AFTER-ROW-FILL", "AddIdField").

        SUPER:ReadData(filter).
      END.
    END METHOD.

    /* Other CUD and Submit operation methods */
    . . .

    METHOD PRIVATE VOID JFPFillMethod(INPUT filter AS CHARACTER):

      DEFINE VARIABLE jsonParser     AS ObjectModelParser      NO-UNDO.
      DEFINE VARIABLE jsonObject     AS JsonObject             NO-UNDO.
      DEFINE VARIABLE cWhere         AS CHARACTER              NO-UNDO.
      DEFINE VARIABLE hQuery         AS HANDLE                 NO-UNDO.
      DEFINE VARIABLE lUseReposition AS LOGICAL                NO-UNDO.
      DEFINE VARIABLE iCount         AS INTEGER                NO-UNDO.
      DEFINE VARIABLE ablFilter      AS CHARACTER              NO-UNDO.
      DEFINE VARIABLE id             AS CHARACTER INITIAL ?    NO-UNDO.
      DEFINE VARIABLE iMaxRows       AS INTEGER   INITIAL ?    NO-UNDO.
      DEFINE VARIABLE iSkipRows      AS INTEGER   INITIAL ?    NO-UNDO.
      DEFINE VARIABLE cOrderBy       AS CHARACTER INITIAL ""   NO-UNDO.

      /* purge any existing data */
      EMPTY TEMP-TABLE ttCustomer.

      jsonParser  = NEW ObjectModelParser().
      jsonObject  = CAST(jsonParser:Parse(filter), jsonObject).
      iMaxRows    = jsonObject:GetInteger("top")  NO-ERROR.
      iSkipRows   = jsonObject:GetInteger("skip") NO-ERROR.
      ablFilter   = jsonObject:GetCharacter("ablFilter") NO-ERROR.
      id          = jsonObject:GetCharacter("id") NO-ERROR.
      cOrderBy    = jsonObject:GetCharacter("orderBy") NO-ERROR.
      cWhere      = "WHERE " + ablFilter NO-ERROR.

      IF cOrderBy > "" THEN DO:
        cOrderBy = REPLACE(cOrderBy, ",", " by ").
        cOrderBy = "by " + cOrderBy + " ".
        /* NOTE: id and seq fields should be removed from 
           cWhere and cOrderBy */
        cOrderBy = REPLACE(cOrderBy, "by id desc", "").
        cOrderBy = REPLACE(cOrderBy, "by id ", "").
        cOrderBy = REPLACE(cOrderBy, "by seq desc", "").
        cOrderBy = REPLACE(cOrderBy, "by seq ", "").
      END.

      lUseReposition = iSkipRows <> ?.

      IF iMaxRows <> ? AND iMaxRows > 0 THEN DO:
        BUFFER ttCustomer:HANDLE:BATCH-SIZE = iMaxRows.
      END.
      ELSE DO:
        IF id > "" THEN
            BUFFER ttCustomer:HANDLE:BATCH-SIZE = 1.
        ELSE                                 
            BUFFER ttCustomer:HANDLE:BATCH-SIZE = 0.
      END.                        

      BUFFER ttCustomer:ATTACH-DATA-SOURCE(DATA-SOURCE srcCustomer:HANDLE).

      IF cOrderBy = ? THEN cOrderBy = "".
      cWhere = IF cWhere > "" THEN (cWhere + " " + cOrderBy) 
               ELSE ("WHERE " + cOrderBy).
      DATA-SOURCE srcCustomer:FILL-WHERE-STRING = cWhere.

      IF lUseReposition THEN DO:
        hQuery = DATA-SOURCE srcCustomer:QUERY.
        hQuery:QUERY-OPEN.

        IF id > "" AND id <> "?" THEN DO:
          hQuery:REPOSITION-TO-ROWID(TO-ROWID(id)).
        END.
        ELSE IF iSkipRows <> ? AND iSkipRows > 0 THEN DO:
          hQuery:REPOSITION-TO-ROW(iSkipRows).
          IF NOT AVAILABLE Customer THEN
            hQuery:GET-NEXT() NO-ERROR.
        END.

        iCount = 0.
        REPEAT WHILE NOT hQuery:QUERY-OFF-END AND iCount < iMaxRows:
          hQuery:GET-NEXT () NO-ERROR.
          IF AVAILABLE Customer THEN DO:
            CREATE ttCustomer.
            BUFFER-COPY Customer TO ttCustomer.
            ASSIGN  ttCustomer.id  = STRING(ROWID(Customer))
                    iSeq = iSeq + 1
                    ttCustomer.seq = iSeq.
          END.
          iCount = iCount + 1.
        END.
      END.
      ELSE DO:
        IF id > "" THEN DATA-SOURCE srcCustomer:RESTART-ROWID(1) 
                        = TO-ROWID ((id)).
        BUFFER ttCustomer:SET-CALLBACK ("AFTER-ROW-FILL", "AddIdField").
        DATASET dsCustomer:FILL().
      END.

      FINALLY:
        BUFFER ttCustomer:DETACH-DATA-SOURCE().
      END FINALLY. 
  
    END METHOD.
    
    METHOD PUBLIC VOID AddIdField (INPUT DATASET dsCustomer):
      ASSIGN  ttCustomer.id = STRING(ROWID(Customer))
              iSeq = iSeq + 1
              ttCustomer.seq = iSeq.
    END.

    @openapi.openedge.export(type="REST", useReturnValue="false", writeDataSetBeforeImage="false").
    @progress.service.resourceMapping(type="REST", operation="count", 
                                      URI="/MyCount?filter=~{filter~}", 
                                      alias="", mediaType="application/json").
    METHOD PUBLIC VOID MyCount( INPUT filter AS CHARACTER, OUTPUT numRecs AS INTEGER):
        DEFINE VARIABLE jsonParser   AS ObjectModelParser   NO-UNDO.
        DEFINE VARIABLE jsonObject   AS JsonObject          NO-UNDO.
        DEFINE VARIABLE ablFilter    AS CHARACTER           NO-UNDO.
        DEFINE VARIABLE cWhere       AS CHARACTER           NO-UNDO.
        DEFINE VARIABLE qh           AS HANDLE              NO-UNDO.

        IF filter BEGINS "WHERE " THEN
            cWhere = filter.
        ELSE IF filter BEGINS "~{" THEN 
        DO:
            jsonParser  = NEW ObjectModelParser().
            jsonObject  = CAST(jsonParser:Parse(filter), jsonObject).
            ablFilter   = jsonObject:GetCharacter("ablFilter") NO-ERROR.
            cWhere      = "WHERE " + ablFilter.
        END.
        ELSE IF filter NE "" THEN
        DO:
            /* Use filter as WHERE clause */
            cWhere = "WHERE " + filter.
        END.

        IF cWhere = ? OR cWhere = "?" THEN cWhere = "".
        CREATE QUERY qh.
        qh:SET-BUFFERS(BUFFER Customer:HANDLE).
        qh:QUERY-PREPARE("PRESELECT EACH Customer " + cWhere).
        qh:QUERY-OPEN ().
        numRecs = qh:NUM-RESULTS.

    END METHOD.

END CLASS.

Key changes to note in Customer.cls include the following:

  • Added statement: USING Progress.Json.ObjectModel.*. — Supports access to the ABL core classes for parsing the JSON Filter Pattern object returned in the filter parameter of the ReadCustomer( ) method.
  • Added @openapi.openedge.method.property annotations: (name="mappingType", value="JFP") and (name="capabilities", value="ablFilter,top,skip,id,orderBy") — Causes the JSDO created from this Business Entity to intercept the value the Kendo UI DataSource passes to the filter parameter of ReadCustomer( ) and convert it to a JSON Filter Pattern object. Without this annotation, the DataSource passes a value to the filter parameter that is a JSON duplicate of the Kendo UI-proprietary settings most recently provided by the filter configuration property or the filter( ) method on the DataSource.
  • Updated statement in the ReadCustomer( ) method: IF filter BEGINS "~{" THEN ... ELSE ... — If the filter parameter value starts with a left brace, invokes an added method (JFPFillMethod( )) to handle an anticipated JSON Filter Pattern; otherwise, the BATCH-SIZE attribute on the buffer handle for ttCustomer is set to return all records in the result set, the AddIdField( ) method is registered as a callback for the AFTER-ROW-FILL event on dsCustomer to initialize the id and seq fields of each record in the result set, and the filter parameter is passed to the ReadData( ) method of the inherited OpenEdge.BusinessLogic.BusinessEntity abstract class to handle another filter string format specified when not using Kendo UI to access the Read operation. (Note that JFPFillMethod( ) also sets different values for BATCH-SIZE based on the filter settings before registering AddIdField( ).)
  • Added method: JFPFillMethod( ) — Parses the property values from the JSON Filter Pattern passed to the filter parameter, assigning any that are found to corresponding ABL variables. Any of these variables that contain appropriate values are then used to implement the filtering, sorting, and paging options that are specified. These values determine the BATCH-SIZE to return in the ttCustomer temp-table for a successful result. Thus, a successful result returns either a single record identified by id, a specified page of records (iMaxRows > 0), or the entire result set of records in the ttCustomer temp-table of the DATASET dsCustomer parameter passed as output from the ReadCustomer( ) method. The record, or set of records, returned represent the result from the specified filtering, sorting, and paging options, if any. Note that an ABL query is used for some options, while the FILL( ) method on dsCustomer is used for others to copy Customer data to ttCustomer and update the corresponding id and seq fields.
  • Added callback method: AddIdField( ) — With this callback registered by either ReadCustomer( ) or JFPFillMethod( ) in response to the AFTER-ROW-FILL event on dsCustomer, this method assigns the current sequence number (seq) and ROWID value (id) of the corresponding database record whose Customer fields have just been copied (using FILL( )) into the corresponding fields of the current ttCustomer record.
  • Added method: MyCount( ), added as a Data Object Count operation to return the total number of records in the server result set — Executed as part of returning a server page to the Kendo UI DataSource, this method identifies any WHERE string in the filter parameter and adds it to a PRESELECT query on the target database table that it constructs and opens. (Note that it sets the string returned by filter to "" if its value is otherwise unspecified and set to the Unknown value (?).) It then passes the value of the NUM-RESULTS attribute on the opened query to its output parameter to provide the total number of records to Kendo UI.
    Note: If you do not add a method like this to the Business Entity and annotate it (in Developer Studio) as a Count operation, the JSDO throws an exception when the Kendo UI DataSource tries to reference the method as part of reading a server page.