9 Use Oracle JET REST Data Provider APIs

Use Oracle JET's REST Data Provider APIs (RESTDataProvider and RESTTreeDataProvider) to send queries to JSON-based REST services and fetch the resulting data to your app.

The RESTDataProvider implements Oracle JET’s DataProvider interface while the RESTTreeDataProvider implements the TreeDataProvider interface. We’ll first describe RESTDataProvider as it is the foundation API to fetch data from JSON-based REST services using the Fetch API. The RESTTreeDataProvider builds on the RESTDataProvider to retrieve hierarchical data.

About the Oracle JET REST Data Provider

Use RESTDataProvider when you want to send query parameters to the JSON-based REST service so that the subset of data that matches your query is returned to the JET app rather than all data.

Use it, if, for example, you want to display a list of employees in your app who match a certain criterion, such as members of a department. You could fetch the full list of employees to your JET app where you query the returned data before you display the data rows that match your criterion. This is inefficient because you send a request for data that you don’t need. Instead, using an instance of RESTDataProvider, you construct a request to the REST service that returns the appropriate subset of data.

RESTDataProvider accomplishes this through the use of transforms, which is a property option that you specify when you initialize a RESTDataProvider instance. RESTDataProvider has three transforms property options. These are fetchFirst, fetchByOffset and fetchByKeys. These correspond to the three available fetch methods on data providers. Each property option defines the following functions:

  • request: A function that creates a Request object to use for the Fetch API call to the the REST API. This is where any required query parameters for paging, filtering, and sorting can be applied to the URL which can then be used to create a request. Other request options, such as headers, body and method can also be added to the request if needed.
  • response: A function that extracts the data and other relevant values from the response body. The function must, at a minimum, return an object with a data property that is an array of items of type D (generic passed into RESTDataProvider class). The generic type D corresponds to the type of the entries in the loaded data. For example, for personal information data, you may have entries of type string and number, as in the following entries: { name: string, age: number }.

The Oracle JET Cookbook includes a series of demos that show usage of the RESTDataProvider. The Overview demo shows you how to initialize an instance of RESTDataProvider using the fetchFirst method that includes request and response functions. See REST Data Provider in the Oracle JET Cookbook and REST Data Provider in the Oracle® JavaScript Extension Toolkit (JET) API Reference for Oracle JET.

About the Oracle JET REST Tree Data Provider

Use RESTTreeDataProvider when you want to retrieve hierarchical data from JSON-based REST services.

The main difference between RESTDataProvider and RESTTreeDataProvider is that RESTTreeDataProvider exposes an additional instance method, getChildDataProvider, and a constructor option that is also named getChildDataProvider. They have different signatures.

  • Instance method's signature is getChildDataProvider(parentKey: K): TreeDataProvider<K, D> | null
  • Constructor option's signature is getChildDataProvider(item: Item<K, D>): DataProvider<K, D> | null

The getChildDataProvider method takes as an argument the key of the parent node to create a child data provider for. It then returns a RESTTreeDataProvider instance that loads the children of the parent node or null if the node is a leaf node. A leaf node is a node that cannot have child nodes.

If the key of the parent node that you pass to getChildDataProvider does not correspond to a previously fetched item or is an item that metadata identifies as a leaf node, getChildDataProvider returns null. Note that calls to getChildDataProvider(parentKey: K) internally call getChildDataProvider(item: Item<K, D>) after retrieving the item corresponding to parentKey. Therefore, it is the constructor option that accesses the metadata of the item corresponding to a node's key. Since the constructor option is defined by the JET app, it is the JET app, and not RESTTreeDataProvider, that decides whether to return null or a DataProvider instance. This whole flow, which starts from the JET app returning metadata through the fetch response transform, is a mechanism to give apps enough information to determine whether a node has children or not.

Note too that in the Oracle JET Cookbook the leaf field name is based on the metadata provided by the cookbook’s mock server through the response transform. Your REST service is likely to use a different field name.

The metadata used by the Oracle JET Cookbook’s mock server is of type { key: K, leaf: boolean}[]. This indicates whether the corresponding node is a leaf node. This is useful when invoking getChildDataProvider. It returns null when the parent node is a leaf node. Otherwise, the node renders with an expansion arrow in the oj-tree-view component even though it cannot have child nodes.

One other thing to note is that create operations for the RESTTreeDataProvider need to use the parentKeys option. This can be seen in the addChildNode method of the Events demo in the Oracle JET Cookbook entry for REST Tree Data Provider. See more detail about the parentKeys option in Data Provider Add Operation Event Detail of the Oracle® JavaScript Extension Toolkit (JET) API Reference for Oracle JET.

The Oracle JET Cookbook includes a series of demos that show usage of the RESTTreeDataProvider API. The Overview demo shows you how to create an instance of RESTTreeDataProvider using the getChildDataProvider method. See REST Tree Data Provider. In addition to the demos in the Oracle JET Cookbook, see also the entry for the REST Tree Data Provider in the Oracle® JavaScript Extension Toolkit (JET) API Reference for Oracle JET.

Create a CRUD App Using Oracle JET REST Data Providers

Use the Oracle JET REST Data Provider APIs to create apps that perform CRUD (Create, Read, Update, Delete) operations on data returned from a REST Service API.

Unlike the Common Model and Collection APIs that we recommended you to use prior to release 11, RESTDataProvider and RESTTreeDataProvider do not provide methods such as Collection.remove or Model.destroy. Instead you use the Fetch API and the appropriate HTTP request method to send a request to the REST service to perform the appropriate operation. In conjunction with this step, you use the data provider's mutate method to update the data provider instance.

Note:

The steps that follow make specific reference to RESTDataProvider, but the general steps also apply to RESTTreeDataProvider. View the Events demo in the Oracle JET Cookbook entry for REST Tree Data Provider to see an implementation of CRUD-type functionality that uses RESTTreeDataProvider. Note that the demos in the Oracle JET Cookbook use a mock REST server, so operations and responses in an actual REST service may differ from Cookbook demonstration.

Define the Data Model for REST Data Provider

Identify the data source for your app and create the data model.

  1. Identify your data source and examine the data. For data originating from a REST service, identify the service URL and navigate to it in a browser.

    The following example shows a sample of the response body for a request sent to a REST service for department data.

    [
      {
        "DepartmentId": 20,
        "DepartmentName": "HR",
        "LocationId": 200,
        "ManagerId": 300,
        "Date": "01 Oct 2015"
      },
      {
        "DepartmentId": 100,
        "DepartmentName": "Facility",
        "LocationId": 200,
        "ManagerId": 300,
        "Date": "13 Oct 2002"
      }
    ]
    

    In this example, each department is identified by the DepartmentId attribute.

  2. Add code to your app that creates an instance of the REST data provider and fetches data from the REST service.
    import { RESTDataProvider } from "ojs/ojrestdataprovider";
    import "ojs/ojtable";
    
    type D = { DepartmentId: number; DepartmentName: string; Date: string };
    type K = D["DepartmentId"];
    
    class ViewModel {
    
        dataprovider: RESTDataProvider<K, D>;
        keyAttributes = "DepartmentId";
        url = "https://restServiceURL/departments";
    
        constructor() {
            this.dataprovider = new RESTDataProvider({
                keyAttributes: this.keyAttributes,
                url: this.url,
    
                transforms: {
                    fetchFirst: {
                        request: async (options) => {
                            const url = new URL(options.url);
                            const { size, offset } = options.fetchParameters;
                            url.searchParams.set("limit", String(size));
                            url.searchParams.set("offset", String(offset));
                            return new Request(url.href);
                        },
                        response: async ({ body }) => {
                            const { items } = body;
                            // If the response body returns, for example, "items". 
                            // We need to assign "items" to "data"
                            return { data: items };
                        },
                    },
                },
            });
        }
    }
    
    export = ViewModel;

Read Records

To read the records, define the Oracle JET elements that will read the records in your app's page.

The following sample code shows a portion of the html file that displays a table of records using the oj-table element and the data attribute that references the dataprovider. In this example, the table element creates columns for Department Id, Department Name, and so on.

<oj-table id="table" data="[[dataprovider]]"
      columns='[{"headerText": "Department Id",
		 "field": "DepartmentId"},
		{"headerText": "Department Name",
		 "field": "DepartmentName"},
		{"headerText": "Location Id",
		 "field": "LocationId"},
		{"headerText": "Manager Id",
		 "field": "ManagerId"}]'>
</oj-table>

Create Records

To add the ability to create new records, add elements to your HTML page that accept input from the user and create a function that sends the new record to the REST service.

  1. Add elements to the app's HTML page that accept input from the user.

    The code in the following example adds oj-input-* elements to an HTML page to allow a user to create a new entry for a department.

    <oj-form-layout readonly="false" colspan-wrap="wrap" max-columns="1">
      <div>
        <oj-input-number id="departmentIdInput" label-hint="Department Id" value="{{inputDepartmentId}}"></oj-input-number>
        <oj-input-text id="departmentNameInput" label-hint="Department Name" value="{{inputDepartmentName}}"></oj-input-text>
        <oj-input-number id="locationIdInput" label-hint="Location Id" value="{{inputLocationId}}const addedRowKey = addedRow[this.keyAttributes]"></oj-input-number>
        <oj-input-number id="managerIdInput" label-hint="Manager Id" value="{{inputManagerId}}"></oj-input-number>
      </div>
      <div>
        <oj-button id="addButton" on-oj-action="[[addRow]]" disabled="[[disabledAdd]]">Create</oj-button>
        . . . 
      </div>
    </oj-form-layout>

    The oj-button's on-oj-action attribute is bound to the addRow function which is defined in the next step.

  2. Add code to the ViewModel to send the user’s input as a new request to the REST service and update the RESTDataProvider instance using the mutate method.
    // add to the observableArray
      addRow = async () => {
        // Create row object based on form inputs
        const row = {
          DepartmentId: this.inputDepartmentId(),
          DepartmentName: this.inputDepartmentName(),
          LocationId: this.inputLocationId(),
          ManagerId: this.inputManagerId(),
        };
        // Create and send request to REST service to add row
        const request = new Request(this.restServerUrl, {
          headers: new Headers({
            "Content-type": "application/json; charset=UTF-8",
          }),
          body: JSON.stringify(row),
          method: "POST",
        });
        const response = await fetch(request);
        const addedRow = await response.json();
        // Create add mutate event and call mutate method
        // to notify dataprovider consumers that a row has been
        // added
        const addedRowIndex = addedRow.index;
        delete addedRow.index;
        const addedRowKey = addedRow[this.keyAttributes];
        const addedRowMetaData = { key: addedRowKey };
        this.dataprovider.mutate({
          add: {
            data: [addedRow],
            indexes: [addedRowIndex],
            keys: new Set([addedRowKey]),
            metadata: [addedRowMetaData],
          },
        });
      };

Update Records

To add the ability to update records, add elements to your HTML page that accept input from the user and create a function that sends the updated record to the REST service.

  1. Add elements to the app page that identify updatable elements and enables the user to perform an action to update them.

    The code in the following example adds oj-input-* elements to an HTML page to allow a user to update a selected entry for a department.

    <oj-form-layout readonly="false" colspan-wrap="wrap" max-columns="1">
      <div>
        <oj-input-number id="departmentIdInput" label-hint="Department Id" value="{{inputDepartmentId}}"></oj-input-number>
        <oj-input-text id="departmentNameInput" label-hint="Department Name" value="{{inputDepartmentName}}"></oj-input-text>
        <oj-input-number id="locationIdInput" label-hint="Location Id" value="{{inputLocationId}}"></oj-input-number>
        <oj-input-number id="managerIdInput" label-hint="Manager Id" value="{{inputManagerId}}"></oj-input-number>
      </div>
      <div>
        <oj-button id="addButton" on-oj-action="[[updateRow]]" disabled="[[ disabledUpdate]]">Update</oj-button>
        . . . 
      </div>
    </oj-form-layout>
    

    The oj-button's on-oj-action attribute is bound to the updateRow function which is defined in the next step.

  2. Add code to the ViewModel to send the user's update as a new request to the REST service and update the RESTDataProvider instance using the mutate method.
    // used to update the fields based on the selected row
      updateRow = async () => {
        const currentRow = this.selectedRow;
        if (currentRow != null) {
          // Create row object to update based on form inputs
          const row = {
            DepartmentId: this.inputDepartmentId(),
            DepartmentName: this.inputDepartmentName(),
            LocationId: this.inputLocationId(),
            ManagerId: this.inputManagerId(),
          };
          // Create and send request to update row on the REST service
          const request = new Request(
            `${this.restServerUrl}/${this.selectedKey}`,
            {
              headers: new Headers({
                "Content-type": "application/json; charset=UTF-8",
              }),
              body: JSON.stringify(row),
              method: "PUT",
            }
          );
          const response = await fetch(request);
          const updatedRow = await response.json();
          const updatedRowIndex = updatedRow.index;
          delete updatedRow.index;
          // Create update mutate event and call mutate method
          // to notify dataprovider consumers that a row has been
          // updated
          const updatedRowKey = this.selectedKey;
          const updatedRowMetaData = { key: updatedRowKey };
          this.dataprovider.mutate({
            update: {
              data: [updatedRow],
              indexes: [updatedRowIndex],
              keys: new Set([updatedRowKey]),
              metadata: [updatedRowMetaData],
            },
          });
        }
      };

Delete Records

To add the ability to delete records, add elements to your HTML that accept input from the user and create a function that sends the record for deletion to the REST service.

  1. Add elements to the app page that identifies records marked for deletion and enables the user to perform an action to delete them.

    The oj-button's on-oj-action attribute in the following example is bound to the removeRow function which is defined in the next step.

    <oj-button id="removeButton" on-oj-action="[[removeRow]]" disabled="[[disabledRemove]]">Remove</oj-button>
  2. Add code to the ViewModel to delete the record or records submitted by the user.
    // used to remove the selected row
    removeRow = async () => {
      const currentRow = this.selectedRow;
      if (currentRow != null) {
        // Create and send request to delete row on REST server
        const request = new Request(
          `${this.restServerUrl}/${this.selectedKey}`,
          { method: "DELETE" }
        );
        const response = await fetch(request);
        const removedRow = await response.json();
        const removedRowIndex = removedRow.index;
        delete removedRow.index;
        // Create remove mutate event and call mutate method
        // to notify dataprovider consumers that a row has been
        // removed
        const removedRowKey = removedRow[this.keyAttributes];
        const removedRowMetaData = { key: removedRowKey };
        this.dataprovider.mutate({
          remove: {
            data: [removedRow],
            indexes: [removedRowIndex],
            keys: new Set([removedRowKey]),
            metadata: [removedRowMetaData],
          },
        });
      }
      this.disabledUpdate(true);
      this.disabledRemove(true);
    };