REST

Labels
AJAX(112) Apple(1) Application Builder(242) Application Factory(207) ASP.NET(95) ASP.NET 3.5(45) ASP.NET Code Generator(72) ASP.NET Membership(28) Azure(18) Barcodes(3) BLOB(18) Business Rules(1) Business Rules/Logic(140) BYOD(13) Caching(2) Calendar(5) Charts(29) Cloud(14) Cloud On Time(2) Cloud On Time for Windows 7(2) Code Generator(54) Collaboration(11) command line(1) Conflict Detection(1) Content Management System(11) COT Tools for Excel(26) CRUD(1) Custom Actions(1) Data Aquarium Framework(122) Data Sheet(9) Data Sources(22) Database Lookups(50) Deployment(22) Designer(177) DotNetNuke(12) EASE(20) Email(6) Features(99) Firebird(1) Form Builder(14) Globalization and Localization(6) Hypermedia(2) Installation(4) JavaScript(20) Kiosk(1) Low Code(3) Mac(1) Many-To-Many(4) Maps(6) Master/Detail(36) Microservices(4) Mobile(63) Mode Builder(3) Model Builder(3) MySQL(10) Native Apps(5) News(15) OAuth(5) OAuth Scopes(1) OAuth2(6) Offline(14) Oracle(10) PKCE(1) PostgreSQL(2) QR codes(2) Rapid Application Development(5) Reading Pane(2) Release Notes(163) Reports(48) REST(26) RESTful(21) RESTful Workshop(13) RFID tags(1) SaaS(7) Security(75) SharePoint(12) SPA(5) SQL Anywhere(3) SQL Server(26) Stored Procedure(4) Teamwork(15) Tips and Tricks(81) Tools for Excel(2) Touch UI(93) Transactions(5) Tutorials(183) Universal Windows Platform(3) User Interface(331) Video Tutorial(37) Web 2.0(100) Web App Generator(101) Web Application Generator(607) Web Form Builder(39) Web.Config(9) Workflow(28)
Archive
Blog
REST
Tuesday, May 17, 2022PrintSubscribe
Data Caching, RESTful API Engine, Visual Studio 2022

Code On Time release 8.9.24.0 introduces the performance enhancing Universal Data Caching and one of a kind RESTful API Engine. Visual Studio 2022 is supported in the Project Designer.

Universal Data Caching will give your applications a significant performance boost. Most databases have a few tables with the content that does not change frequently. Developers can define simple rules that will eliminate the queries to such tables. It takes only a few minutes to make your app run faster.

Specify the data controllers and and the output caching duration in the application configuration. Start reaping the benefits!

Level 3 RESTful API Engine is included now in every application built with Code On Time. The data controllers are automatically mirrored in the application API.

  • Developers can use the engine to create custom data-aware embedded pages that either blend in the Touch UI or have a completely custom presentation.
  • The engine supports OAuth 2.0 Authorization Flows suitable for the standalone web and mobile applications. Use any framework and technology to build a custom frontend with the Code On Time application in the backend.
  • External scripts and utilities can make direct calls into your application with the robust authentication based on authorization keys. Use tools like cUrl to make your application perform custom actions or change data.
  • Developers can limit the application UI to the specific user roles. For example, administrators can sign into the application and manage data while the users in other roles will only be able to sign in to confirm their identity during OAuth 2.0 Authorization.

The engine is introduced in the RESTful Workshop, the curated set of tutorials that will empower the reader to build the modern high performance software in a record time and at the fraction of the cost of other tools. Level 3 REST API is self-documented with the hypermedia embedded in the data.

RESTful API Engine makes it possible to build custom embedded pages and standalone web and mobile frontends using your application as a backend. External scripts and utilities can use your application to orchestrate the complex workflows.

Code On Time is making a big bet on the RESTful API Engine to deliver the new benefits to our customers:

  • Developers will be able to build apps serving as backends for devices that need to know the user identity and a way to read and write the user-specific data. The next release will support OAuth 2.0 Device Authorization Flow for the input-constrained devices. You may have experienced this flow when authorizing a medical device to access your health data or when allowing a smartwatch to use your music service. The same flow is authorizing a smart TV in a hotel room to access the video streaming service that you are paying for at home.
  • Cloud Identity has been on our roadmap for several years. Soon developers will be able to build a collection of apps with one of them serving as the Identity Provider while the others will become the Identity Consumers.
    • Each application will have its own security system.
    • Users will identify themselves by signing into the Identity Provider. This app may have the 2-Factor Authentication enabled or include an option to authenticate users with the third-party providers such as Google, Microsoft, or Facebook.
    • An application will become the identity consumer at runtime when the administrator creates a Cloud Identity link with another application. Device Authorization Flow will authorize the establishment of the link when approved by the administrator of the intended Identity Provider.
    • Identity Consumer application synchronizes its roles and custom OAuth 2.0 scopes with the Identity Provider database. This makes it possible to manage users of the entire application collection in one place.
    • RESTful API Engine access tokens will be issued by the Identity Provider application. The same access token will authorize requests to any application in the collection. The token will inherit the permissions configured in the Identity Provider by the collection administrator.
  • Microservices Architecture becomes a reality with Code On Time even for the smallest of developer shops. Cloud Identity and access token portability across the collection of applications make it possible to create the frontend that will exchange data with multiple backend applications. Each backend provides a custom API, has its own performance characteristic, and can be deployed independently. This improves the stability of the frontend and its overall performance. Learn more in the Introduction to Microservices Architecture.
  • The upcoming built-in GraphQL processor will parse the queries and resolve them by executing the RESTful API Engine requests. GraphQL has emerged at Facebook as the query language for the complex collections of APIs. Their numerous apps needed small subsets of data from multiple sources, which required several requests fetching too much data. Graph QL server is the “fat client” that defines the supported query types. The server accepts the queries formulated as a graph and delegates the execution to the custom resolvers. The output of multiple resolvers is merged in the graph that matches the query.
  • The App Studio application is a part of each Code On Time installation. This local web app presently starts the product activation process by redirecting developers to the account management portal. App Studio will come with the RESTful API Engine enabled in the future releases. It will serve as the local project management portal that will replace the start page of the app generator, which will be relegated to the icon tray. It will also provide the backend to the v9 Live Project Designer. The code generator will be invoked in the command line mode by the RESTful API Engine of the App Studio in response to the developer activities in the Project Designer of live apps. This is the architectural departure from our original plan outlined in the Roadmap, but we are on the finishing line now to deliver the amazing development tools in the heads of developers.
  • Content Hub, our innovative publishing platform, creates the new content in our legacy public site. We have developed this platform for internal use. The hub agent monitors the shared drives in our corporate Google Drive and transforms the changed documents into the HTML-based Display Flow content published through the RESTful API to the account management portat. The portal feeds the content to the database of the legacy site. The following capabilities of the platform will become commercially available:
    • Content Hub Agent will monitor Google Drive, Office 365, and network folders for the content in Microsoft Word Format. The agent is a multi-platform application. Changes are pushed to the hub-enabled application built with Code On Time.
    • Content Hub Add-On for applications created with Code On Time will render the content imported by the hub agent as public pages, blog posts, documentation library, newsletters, helpdesk tickets, public forum posts, etc.
    • Add-on will provide the interactive content editor based on Touch UI. It will customize the presentation style of the live “content hub” pages. It will create tickets and community posts from within the apps built with Code On Time in the same format that is used by the hub agent. It will invoke the RESTful API of the app to read and write the hub content.

The release 8.9.24.0 includes the following features and enhancements:

  • (Framework) Upgraded the framework to jQuery.3.6.0 and the latest version of the Material Icons.
  • (AppGen) Added support for VS2022.
  • (App Gen) Develop command in the project options will activate the most recent version of Visual Studio if the solution file format version is 12.00. This will ensure that VS2022 starts when both VS2022 and 2019 are installed on the same machine.
  • The enhanced Postal Address Verification silently resolves the postal addresses with Google Geocoding. The customers in the United States can achieve a perfect resolution of each address in the USPS-approved format.
  • (Touch UI) New tag form-max-(xs|sm|md) will make the contents of the form aligned in the middle of the page.
  • (Touch UI) Input field tagged as text-style-primary will display in the "primary" color.
  • (Touch UI) Modal popups will have the header when displayed in the content pages, which have their own header hidden.
  • (DAF) Client-side cookies are created with the SameSite=Strict attribute.
  • (Framework) Access control check is performed in the correct sequence for the standard membership controllers both for reading and writing of data.
  • (Touch UI) New css rules to enable hover over the fields with the Actions lookup to ensure correct display of icons in the buttons.
  • (Touch UI) Right-clicking on the logo will open the menu panel.
  • RESTful API Engine is included in the generated apps.
  • (Framework) New js/sys/restful.js provides a thin wrapper on top of Fetch API for the RESTful API Engine.
  • (Touch UI) A click on the empty part of the app toolbar or reading pane button bar will close an visible popup menu.
  • (Touch UI) Eliminated the redundant 3rd level child Data View fetching in the form views with multi-level master-detail. This significantly reduces the load time of forms with master-detail field relationships.
  • (Touch UI) Improved stability of inline editing in multi-level master detail forms. In some situations the aggressive selection of items in DataView fields would have caused the unnecessary attempts to fetch data and server-side exceptions.
  • Universal Data Caching improves the response time of the requests to read data in Touch UI.
  • (Touch UI) The focused universal inputs are redrawn whether or not they have a value. Previously only non-empty inputs were redrawn.
  • (Touch UI) Simplified parsing of the virtual page activator.
  • Integrated codemirror-5.65.2 for the upcoming Content Hub Add-On to allow display and editing of code samples.
  • (Touch UI) Refactored the code responsible for the "scrollable" state of the system tab bar. The visual presentation goes from flat to scrolled when the scrollable content is detected and the view does not have the horizontal scrolling (grid with horizontal scrolling).
  • (Touch UI) Removed redundant CSS class definition.
  • (Framework) New $app.urlArgs() method returns an object map of the page location URL parameters of the specified argument. The argument is either a string or a hypermedia object with the 'href' property. If the parameter is not specified, then the URl of the page in the browser is used for the parameter extraction.
  • (Touch UI) Account Access grant for OAuth 2.0 Authorization Flows is now supported by the framework,
  • (Offline Sync) Improved reporting of data downloading errors during sync.
  • (Framework) ASP.NET version headers and X-Powered-By headers are not returned by the apps in the HTTP response headers.
  • (Client Framework) Server-side errors raised when the data is retrieved from the server are displayed in the notifiction bar at the bottom of the screen. This helps detect data fetching errors that may remain invisible to the developers.
  • (ASPX) New method Invoke is implemented in DataControllerService to handle all incoming requests. It allows file uploading in the apps with the aspx page model.
  • (Map) Clearing of the geo map will result in the new tooltip assigned to it.
  • (Touch UI) Fixed the exception raised when the quick find filter is cleared.
  • (App Gen) Develop command in the project options will activate the most recent version of Visual Studio if the solution file format version is 12.00. This ensures that VS2022 starts when both VS2022 and VS2019 are installed on the same machine.
  • (Designer) Action property "whenKeySelected"is correctly persisted when set to "No" in the Project Designer.
  • (Site Content) CreatedDate and ModifiedDate fields are set to the DateTime.UtcNow.
  • (CMS) Renamed "Workflow Register" option to "Site Content (recommended)". This option is available in the database connection string settings.
  • (Reports) Class Library projects are provided with the correct ReportViewer references. Previously the leading and trailing spaces have caused errors when building projects.
  • (Reports) Included the reference to the Microsoft RDL Designer for VS 2022 in the custom reports. The reference is visible to the developers in VS if the designer is not installed.
  • (Code Gen) Simplified the syntax of !String.IsNullOrEmpty in the C# source code.
  • (Code Gen) Added support for IsNullOrWhiteSpace unary code generation expression.
  • (CodeGen) Value Inequality operators are produced as natural a != b and a<>b operators in C# and VB. Previously such operators were generated as !(a==b) and Not(a = b).
  • (Code Gen) C# code string constants are now presented with "string" + "string" breaks.
  • (Code Gen) C# code placed to the previous text line the symbols +|-|*|/|% that were previously wrapped to the next line.
  • (Codegen) C# code simplification of "params System.Object[]" to "params object[]".
  • (Framework) Numeric primary keys do not cause hidden exceptions when new BLOBs are uploaded with ODP.
  • (Framework) Method $app.actionInfo(path) returns the information about the action for the specified path in the format "groupId/actionId".
  • (Touch UI) Actions rendered as "Actons" field display simple confirmations and data controller confirmations.
  • (Touch UI) Remove the preloading instructions for the fonts. Added the version of the app to the font reference in the touch-theme.css.
  • (CodeGen) Class attribute definitions in C# will have "spaces" surrounding the "=" preceding the attribute parameter values.
  • (Touch UI) Right-clicking on the logo will open the menu panel.
  • (Touch UI) Click on the empty part of the app toolbar or reading pane button bar will close an visible popup menu.
  • (Touch UI) Eliminated the redundant 3rd level child Data View refresh in the form views with multi-level master-detail.
  • (Touch UI) Improved stability of inline editing in multi-level master detail forms.
Thursday, April 21, 2022PrintSubscribe
BLOBs with RESTful API

Touch UI provides the following view of the Categories in the backend application. The Picture field contents can be downloaded, replaced, or deleted by users. The sketch pad allows making visual annotations on the image.

Images can be downloaded, replaced, or deleted. The BLOB input of Touch UI allows making the visual annotations on the image. The BLOB data is physically stored in the database or external storage.
Images can be downloaded, replaced, or deleted. The BLOB input of Touch UI allows making the visual annotations on the image. The BLOB data is physically stored in the database or external storage.

Every row in the Categories table contains a Binary Large Object (BLOB) in the Picture column. The data type of the column is image. Modern database engines allow storing the binary data of variable length directly in the tables. Applications created with Code On Time work with the BLOBs stored in the database or external storage, such as a folder of the file system or a container in Microsoft Azure.

The JSON data presented below is the categories singleton resource returned by the backend. There are five fields and lots of hypermedia. The picture field and products field have the API links as their values. The former corresponds to the physical column Categories.Picture. The latter is the products field defined in the Categories model.

RESTful API allows fetching the BLOB values, passing BLOBs in the CRUD requests, and replacing or deleting the BLOBs in the singleton resource fields.
JSON
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152{
    "categoryId": 1,
    "categoryName": "Beverages",
    "description": "Soft drinks, coffees, teas, beers, and ales",
    "picture": "/v2/categories/1/picture",
    "products": "/v2/categories/1/products",
    "_links": {
        "self": {
            "href": "/v2/categories/1"
        },
        "up": {
            "href": "/v2/categories"
        },
        "collection": {
            "href": "/v2/categories?count=true"
        },
        "first": {
            "href": "/v2/categories?page=0&limit=10"
        },
        "products": {
            "href": "/v2/categories/1/products?count=true",
            "embeddable": true
        },
        "products-first": {
            "href": "/v2/categories/1/products?page=0&limit=10",
            "embeddable": true
        },
        "edit": {
            "href": "/v2/categories/1",
            "method": "PATCH"
        },
        "replace": {
            "href": "/v2/categories/1",
            "method": "PUT"
        },
        "replace-picture": {
            "href": "/v2/categories/1/picture",
            "method": "PUT"
        },
        "delete-picture": {
            "href": "/v2/categories/1/picture",
            "method": "DELETE"
        },
        "delete": {
            "href": "/v2/categories/1",
            "method": "DELETE"
        },
        "schema": {
            "href": "/v2/categories/1?_schema=true"
        }
    }
}

Fetching BLOBs

The BLOB can be fetched if the value of the singleton resource field is not equal to null. The link in the value identifies the resource followed by the name of the field. The original file name of the BLOB may follow the field name in the resource URL with certain configurations of the resource data controller.

Links in Values

The BLOB link in the sample data shown above consists of the /v2/categories/1 singleton resource URL and the picture field name concatenated together. An attempt to load the blob in the incognito or private browser mode will force the backend application to return the error. An authorization key or access token is required to read the resources known to the RESTful API.

RESTful API must be enabled explicitly in the application configuration. An authorization key or access token is required to access a resource.
RESTful API must be enabled explicitly in the application configuration. An authorization key or access token is required to access a resource.

Navigate to the backend application in the new browser window, sign in, and enter the same URL in the address bar. The image will be loaded successfully . Touch UI application creates an encrypted cookie to track the user identity. The BLOB request will be authenticated since the cookie is sent by the browser with each request.

RESTful API Engine identifies the user automatically when the host application is making a request. The cookie with the self-encrypted user access token provides the identity information.
RESTful API Engine identifies the user automatically when the host application is making a request. The cookie with the self-encrypted user access token provides the identity information.

Standalone single page applications and other HTTP clients will have to specify an authorization key or access token explicitly. Here is the blob fetched by Postman with the authorization key specified in the URL.

Postman shows the BLOB fetched from the resource with the authorization key specified in the "x-api-key" query parameter. RESTful API Engine requires an access token or authorization key to be specified explicitly by the external applications.
Postman shows the BLOB fetched from the resource with the authorization key specified in the "x-api-key" query parameter. RESTful API Engine requires an access token or authorization key to be specified explicitly by the external applications.

Most client applications will not be able to secure an authorization key or set up a cookie with the user identity. JavaScript client apps can establish a secure communication channel with the backend by taking advantage of the restful.js script. It is being used extensively in the standalone single page apps of the workshop.

Here is the customized SPA4 with RESTful Hypermedia and CRUD client app that fetches the images from the backend application in a secure fashion. An image appears in the Category cell when clicked by users.

The standalone single page application fetches the image of the product category on-demand when the category name is clicked. The app follows the hypermedia links to find the product category "picture" field value.
The standalone single page application fetches the image of the product category on-demand when the category name is clicked. The app follows the hypermedia links to find the product category "picture" field value.

The BLOB resource URL cannot be assigned to the src attribute of the image and must be fetched from the backend with the access token specified in the request header.

This is the modified version of the handleHypermedia function that does the image retrieval and assignment.

JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758function handleHypermedia(e) {
    var row = e.target.closest('[data-index]')
    if (row && products) {
        let tableCell = e.target.closest('td');
        if (tableCell.previousSibling != null && tableCell.previousSibling.previousSibling == null) {
            // the cell in the "Category" column was clicked
            $app.restful({
                url: products.collection[row.getAttribute("data-index")]._links.self,
                hypermedia: 'categoryId >>'
            }).then(obj => {
                $app.restful({
                    url: obj.picture
                }).then(blob => {
                    // create an object URL
                    let dataUrl = URL.createObjectURL(blob);
                    // create an image element and replace the cell content with it
                    let img = document.createElement('img');
                    img.src = dataUrl;            // set the image src
                    img.title = obj.categoryName; // set the image tooltip
                    img.style.maxWidth =          // set the maximum image width
                        tableCell.getBoundingClientRect().width - 12 + 'px';
                    // clear the cell content
                    tableCell.innerHTML = '';
                    // insert image in the cell
                    tableCell.appendChild(img);
                });
            });
        }
        else
            readProduct(row.getAttribute("data-index"));
    }
    else {
        var btn = e.target.closest('[data-hypermedia]');
        if (btn && products) {
            var hypermedia = btn.getAttribute('data-hypermedia');
            switch (hypermedia) {
                case 'new':
                    newProduct();
                    break;
                case 'create':
                    postProduct();
                    break;
                case 'edit':
                    patchProduct();
                    break;
                case 'delete':
                    deleteProduct();
                    break;
                case 'cancel':
                    hideForm();
                    break;
                default:
                    refreshProductList(products._links[hypermedia]);
                    break;
            }
        }
    }
}

The original implementation orchestrates the UI flow in the app by responding to the clicks on the product list rows and buttons. The Edit Product form is displayed when users click anywhere in the product row.

The extended code figures when the click occurs in the second column of the product table and goes on to fetch and display the category image in the table cell:

  • First, it invokes the $app.restful method. The self link of the clicked product item is specified in the url parameter. The hypermedia parameter tells the method to locate and fetch the categoryId hypermedia control in the singleton product resource. The obj argument in the then chain function will be the instance of the category object just like the one shown at the top of the tutorial.
  • The obj argument has the picture property. Another call of the $app.restful method will fetch the BLOB object from the url specified in the property value. The blob argument of the then chain function is the instance of the Blob object. The $app.restful method converts the server response into a Blob instance if the content type of the response is not JSON, XML, or YAML.
  • Finally, the Object URL is created from the Blob instance and the table cell contents are replaced with the image that has its src attribute set to base-64 encoded URL. The image is fitted to the cell boundaries and has its title set to the category name to serve as a tooltip.

The products resource collection items do not have the URL of the category picture. The display of the category image takes three requests:

  1. The product item is fetched from the self hypermedia link of the selected item in the product collection.
  2. The category is fetched from the categoryId hypermedia control of the product item.
  3. The BLOB is fetched from the resource specified by the picture field of the product category.
Client application discovers the missing data by following the hypermedia links.

A shrewd developer may figure the correct category image URL and reduce this discovery sequence to a single request:

JavaScript
123456789101112131415161718// the cell in the "Category" column was clicked
Let productItem = products.collection[row.getAttribute("data-index")];
$app.restful({
    url: '/v2/categories/' + productItem.categoryId + '/picture'
}).then(blob => {
    // create an object URL
    let dataUrl = URL.createObjectURL(blob);
    // create an image element and replace the cell content with it
    let img = document.createElement('img');
    img.src = dataUrl;            // set the image src
    img.title = productItem.categoryName; // set the image tooltip
    img.style.maxWidth =          // set the maximum image width
        tableCell.getBoundingClientRect().width - 12 + 'px';
    // clear the cell content
    tableCell.innerHTML = '';
    // insert image in the cell
    tableCell.appendChild(img);
});

The item in the product collection has the categoryId, which can be used to construct the URL of the categories resource singleton field without much difficulty.

The explicit resource URLs in the client application create a tight coupling between the client and the current implementation of the backend. This may lead to broken apps when the backend evolves over time.

If efficiency is important, then consider including the additional fields in the collections and singletons by denormalizing the data models. Avoid using the explicit URLs in the client code to ensure the application longevity and maintainability.

Public Links

Making dedicated requests to fetch an image from the server may seem exotic, but unavoidable if the images cannot be exposed to the world. BLOB objects may represent documents, images, video, and sound files, which frequently require limited access.

The product category image is not something that needs to be kept private. If that is the case, then an authorization key must become a part of the image URL specified in the src attribute of the img element. The job of fetching the image from the server can be outsourced to the browser and Document Object Model.

Let’s introduce the public authorization key in the ~/app/touch-settings.json configuration file of the backend application. Note that there is also the server.rest.acl section specifying the Access Control List of the API.

JSON
123456789101112131415161718192021222324252627282930313233343536{
  "ui": {
    "roles": "Administrators"
  },
  "server": {
    "rest": {
      "enabled": true,
      "output": {
        "json": {
          "indent": true
        }
      },
      "authorization": {
        "keys": [
          {
            "key": "94b79da7-e3a9-4152-9213-886f4c810bec",
            "user": "admin"
          },
          {
            "key": "public",
            "user": "user"
          }
        ]
      },
      "acl": {
        "categories": {
          "administrators": ".",
          "users": "GET"
        },
        "products|suppliers": {
          "administrators": "."
        }
      }
    }
  }
}

The word “public” is the fitting name for the public authorization key. In this example, the user account is associated with the public key.

The authorization key can be specified as the x-api-key or api_key query parameter. There is also a more elegant method of specifying the authorization key in the URL. The key may be included in the path right after the API version like this:

http://localhost:42439/v2/public/categories/8/picture

The URL with the public key in the path will fetch the image in the incognito mode if the access control list allows it.
The URL with the public key in the path will fetch the image in the incognito mode if the access control list allows it.

The word “public” in the key name has no special meaning and can be replaced with any mnemonic. Any authorization key discoverable by the third party is the public key.

Developers must configure an access control list (ACL) to limit the API exposure to the unauthenticated users that will consume the resources with a public authorization key.

The ACL in the configuration above will allow the identities with the Users role to perform the GET requests to the categories resources. Users in the Administrators role can perform requests to the resources of categories, products, and suppliers with any HTTP verb.

The names of the server.rest.acl key properties are the controller rules (regular expressions) that will be evaluated against the names of the data controllers. If the rule of the access control list is matched with the data controller name in the resource path, then the restful API will evaluate the identity rules (the properties of the matched controller rule). The property names are the application user roles or the OAuth 2.0 scopes. If the current user is in role or has the scope in the access token, then the engine will try to match the HTTP method or the custom action identifier with the identity rule value (also a regular expression). If there is no match, then the access is denied.

This is the code that will use the public authorization key to fetch the category images when the category name is clicked in the product grid.

JavaScript
12345678910111213// the cell in the "Category" column was clicked
let productItem = products.collection[row.getAttribute("data-index")];
let img = document.createElement('img');
img.src =                             
    'http://localhost:42439/v2/public/categories/' +  
    productItem.categoryId + '/picture'; // set the image src
img.title = productItem.categoryName;    // set the image tooltip
img.style.maxWidth =                     // set the maximum image width
    tableCell.getBoundingClientRect().width - 12 + 'px';
// clear the cell content
tableCell.innerHTML = '';
// insert image in the cell
tableCell.appendChild(img);

There are no explicit resource fetching requests. A simple assignment to the src property of the img element does the job. The only concern is the hard-coded URL pointing to the backend application.

File Names in Links

The Categories table does not store the attributes of the Picture image file. The original name, type, and length of the file is not known. The BLOB processing code of the framework detects the image data automatically and sets the appropriate content type and a generic file name in the response to the request to fetch a BLOB resource. The generic content type application/octet-stream will describe a BLOB that does not contain an image.

The resource /v2/public/categories/1/picture will fetch the BLOB data even though it appears to lack the physical file name.

Application framework relies on the optional utility fields to provide the extended information about the BLOB fields of a data controller. The field names FileName, ContentType, and Length will be treated as the default utility fields. Developers can be more specific and prefix the default utility field names with the name of the corresponding BLOB field.

Alter the Categories table by adding the FileName, ContentType, and Length columns to store the extended properties of the BLOB in the Picture column. Make sure that the BLOB and the utility columns allow null values.

image3.png

Select the backend application project on the start page of the app generator, choose Refresh, and proceed to incorporate the new database schema in the project metadata. Select the Model option in the summary, choose the Categories model, and select the new columns to include them in the entity data model.

Model Builder makes it easy to perform iterative enhancement of the data models when the database schema is changed.
Model Builder makes it easy to perform iterative enhancement of the data models when the database schema is changed.

Save the Categories model and proceed to generate the app. The new fields will become a part of the user interface. Begin adding a new category and observe how the File Name, Content Type, and Length are automatically set to the properties of the selected Picture file.

Touch UI automatically detects the "utility" fields and captures the File Name, Content Type, and Length of the binary data when a file is selected or dropped on the BLOB input field.
Touch UI automatically detects the "utility" fields and captures the File Name, Content Type, and Length of the binary data when a file is selected or dropped on the BLOB input field.

Changes to the model are also reflected in the RESTful API. Here is how the new categories singleton resource may appear to the apps and developers:

JSON
1234567891011121314151617181920212223242526{
    "_links": {
        "self": {
            "href": "/v2/public/categories/197"
        },
        "up": {
            "href": "/v2/public/categories"
        },
        "collection": {
            "href": "/v2/public/categories?count=true"
        },
        "first": {
            "href": "/v2/public/categories?page=0&limit=10"
        },
        "schema": {
            "href": "/v2/public/categories/197?_schema=true"
        }
    },
    "categoryId": 197,
    "categoryName": "Baked Goods",
    "description": "Breads, cookies, pastries",
    "picture": "/v2/public/categories/MTk3L3BpY3R1cmUvag.jpeg",
    "fileName": "bread.jpeg",
    "contentType": "image/jpeg",
    "length": 13155
}

Note that that picture value now has the file name but lacks the identifier of the singleton. The resource /v2/categories/197/picture transforms to /v2/categories/MTk3L3BpY3R1cmUvag.jpeg in the BLOB value. The URL is not affected by the original file name but inherits the file extension. The extension helps users to understand the nature of data.

The BLOB value with the authorization key in the path can be used in the public-facing websites. The extension of the file name gives a hint about the data type.
The BLOB value with the authorization key in the path can be used in the public-facing websites. The extension of the file name gives a hint about the data type.

If the “FileName” utility field is a part of the data controller, then its resource will have the BLOB value URL ending with the file name composed of its base64-url-encoded BLOB field name preceded by the previous path segment. The encoded file name will have the same extension as the original file.

ETag and 304 Not Modified

The headers of the response to the BLOB resource request will include the ETag (entity tag). The value is the unique hash of the BLOB content. Web browsers capture the ETag and send it with the subsequent requests to fetch the same resource.

The response of the BLOB resource request includes the ETag, Content-Type, Content-Disposition, and Content-Length headers. ETag value can be used to avoid the unnecessary transmission of the unchanged BLOB. Type, Disposition, and Length provide the client with the BLOB metadata.
The response of the BLOB resource request includes the ETag, Content-Type, Content-Disposition, and Content-Length headers. ETag value can be used to avoid the unnecessary transmission of the unchanged BLOB. Type, Disposition, and Length provide the client with the BLOB metadata.

The RESTful API Engine will compare the optional If-None-Match header of the request with the ETag of the resource. If both values are the same, then the engine will not send the BLOB data in the response. The body will be empty and the response status code will be set to 304 Not Modified. The client application will use the previously fetched resource data.

The "304 Not Modified" response includes the ETag of the resource that has not changed since the last fetch.
The "304 Not Modified" response includes the ETag of the resource that has not changed since the last fetch.

Web browsers and views automatically use the If-None-Match and ETag headers, which helps avoid the unnecessary transmission of data.

BLOBs in CRUD Requests

Binary field values can be specified in the CRUD operations with PUT, POST, and PATCH verbs. These HTTP methods correspond to the replace, create, and edit hypermedia controls in the resources of the RESTful API Engine.

The BLOB values can be found in the collection and singleton resources. The values of the BLOB fields are the API resources links.

Multipart Form Data

The content type multipart/form-data tells the server that the binary content is specified in the request body. The HTML forms with the “file” inputs are submitted with this content type in the “formdata” format by the web browsers and views.

Here is how the request with the binary data is constructed in Postman. This request will create a new category. Individual field values are listed as key-value pairs in the form-data tab of the request body. The picture field is configured as the file. The response body shows the new resource with the BLOB link in the picture value.

This is the "create" request in the Postman with the form-data fields in the request and the new category resource in the response. The "file" value is specified in the "picture" field of the request. The properties of the file are automatically captured by the RESTful API Engine in the "fileName", "contentType", and "length" fields of the new resource.
This is the "create" request in the Postman with the form-data fields in the request and the new category resource in the response. The "file" value is specified in the "picture" field of the request. The properties of the file are automatically captured by the RESTful API Engine in the "fileName", "contentType", and "length" fields of the new resource.

The plain JavaScript code that will create a category in the browser page is presented next. It uses the Fetch API to make the POST request. Note the use of the x-api-key header set to the authorization key of the admin user.

JavaScript
12345678910111213141516171819var myHeaders = new Headers();
myHeaders.append("x-api-key", "94b79da7-e3a9-4152-9213-886f4c810bec");

var formdata = new FormData();
formdata.append("categoryName", "Baked Goods");
formdata.append("description", "Breads, cookies, pastries");
formdata.append("picture", fileInput.files[0], "bread.jpeg");

var requestOptions = {
  method: 'POST',
  headers: myHeaders,
  body: formdata,
  redirect: 'follow'
};

fetch("http://localhost:42439/v2/categories", requestOptions)
  .then(response => response.text())
  .then(result => console.log(result))
  .catch(error => console.log('error', error));

The categoryName and description request fields can be specified in the single JSON object as the value of the unnamed field in the form-data. Its content type must be set to application/json. The binary fields of the resource must be specified as the named fields of the form-data. The order of the fields in the form-data is not important.

The non-binary fields of he request are specified in the form-data field without a name in the JSON format.
The non-binary fields of he request are specified in the form-data field without a name in the JSON format.

The raw HTTP request with the multipart/form-data content type is presented in the sample below. The request will create a new category. Client applications can configure HTTP requests on their own or use the libraries available with their chosen implementation technologies.

HTTP
1234567891011121314151617181920POST /v2/categories HTTP/1.1
Host: localhost:42439
x-api-key: 94b79da7-e3a9-4152-9213-886f4c810bec
Content-Length: 408
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="picture"; filename="bread.jpeg"
Content-Type: image/jpeg

(data)
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name=""
Content-Type: application/json

{ 
   "categoryName": "Baked Goods",
   "description": "Breads, cookies, pastries"
}
----WebKitFormBoundary7MA4YWxkTrZu0gW

Method $app.restful

The standalone single page applications SPA4 and SPA5 are linking the lightweight external script with the definition of $app.restful method. The method automatically specifies the access token in the HTTP request headers to identify the user. The method refreshes the expired access token. An explicit resource URL or a hypermedia control can be specified in the url property of the method argument.

The restful.js resource of the backend application provides the standalone clients with a thin wrapper on top of the Fetch API.

The $app.restful method also detects the Blob fields specified in the body property of the argument. The request body with the Blob fields is automatically converted to an instance of the FormData object.

Let’s modify the CRUD-enabled version of SPA4 to create a category with the user-defined image in the picture field. Users will click on their avatar to activate the File Chooser prompt. The selected image file will become the picture of the new “Baked Goods” category. The name and the description of the category will be hard-coded for simplicity.

image6.png

Users can create new products in this category and observe the picture when the Category column is clicked in the product row.

image9.png

Add the following code at the bottom of the closure in the spa4.js. The code will attach the click handler to the avatar when the spa4.html page is loaded in the browser window.

JavaScript
123456789101112131415161718192021222324252627window.addEventListener('load', e => {
    // add 'click' event handler to the user avatar
    document.querySelector('#avatar').addEventListener('click', e => {
        // create a "file" input 
        var fileInput = document.createElement('input');
        fileInput.type = 'file';
        // wait for the "picture" file to be selected
        fileInput.addEventListener('change', e => {
            // create the "Baked Goods" category with the selected picture
            $app.restful({
                url: '/v2/categories',
                method: 'POST',
                body: {
                    categoryName: "Baked Goods",
                    description: "Breads, cookies, pastries",
                    picture: fileInput.files[0] // the picture image
                }
            }).then(newCategory => {
                // show the ID of the new category
                alert(newCategory.categoryId);
            }).catch(restfulException);

        });
        // display the File Chooser prompt in the browser 
        fileInput.click();
    });
});

The code dynamically creates an input of the file type and “clicks” it programmatically. The prompt to select a file is displayed. The change event will trigger on the input when a file is selected by the user. The $app.restful method executes the POST request with the categoryName, description, and picture specified in the body property of the method argument. The picture is set to the selected file.

Replacing and Deleting BLOB Values

The categories resource singleton hypermedia contains the replace-picture and delete-picture controls.

The blob image in the picture field of the resource can be replaced with the $app.restful method that has the replace-picture hypermedia object specified in its url property of the argument. The value field in the body property must be set to the new Blob or File value.

The delete-picture hypermedia control in the url property of the method argument will clear the BLOB value in the picture field.

The pair of the explicit url and method values can replace the hypermedia control in the $app.restful argument properties.

Next

Hypermedia in the resources provides instant documentation for developers and methods to transition the resource state or access the related data for the algorithms.

Learn to navigate the RESTful resources in the development tools and client apps.

Saturday, March 19, 2022PrintSubscribe
Custom Actions with Hypermedia

The REST API Level 2 and above must support HTTP methods (verbs) in resource handling. For example, the request with the GET method is expected to return the resource data while the request with the PATCH method is expected to modify some of its fields. The standard HTTP methods work well for CRUD. Unfortunately the limited number of HTTP verbs will not cover all possible scenarios of data manipulation.

RESTful API Engine allows specifying an action identifier directly in the resource URL of the POST request. The available custom actions are advertised in the resource hypermedia.

Example

Let’s modify the backend application to implement a custom action that will update the inventory when a product is sold.

The interactive users of Touch UI and the algorithmic clients of RESTful API will have the ability to sell products.

Users of the backend will navigate to the Products or Suppliers page, choose a product, and press the Sell button. The popup form will ask users to specify the number of product units to sell. The Units In Stock field value of the selected product will decrease by the number of the sold units.

Confirmation controller "SellPrompt" presents the user with an option to sell 1, 5, or 10 units of a product with a single touch. The "Other" option allows entering the arbitrary quantity of units to sell. The prompt is displayed when users press the "Sell" button in the product form.
Confirmation controller "SellPrompt" presents the user with an option to sell 1, 5, or 10 units of a product with a single touch. The "Other" option allows entering the arbitrary quantity of units to sell. The prompt is displayed when users press the "Sell" button in the product form.

Developers evolve both UI and API by making changes to the data controllers in the Model Builder and Project Designer.

The API users will post the quantity of units to sell to the action URL, which will be available in the hypermedia of the singleton resources of products.

RESTful API Engine makes the custom action "sell" available to the client apps.  The parameters of the action are specified in the "parameters" key of the payload. The new state of the resource is returned in the response.
RESTful API Engine makes the custom action "sell" available to the client apps. The parameters of the action are specified in the "parameters" key of the payload. The new state of the resource is returned in the response.

Here is how the “Sell” user interface feature is correlating with the upcoming application design changes:

  1. The new Sell button will be displayed in the form when a product is selected. The button represents the custom action.
  2. The form Quantity to Sell will be shown when the button is pressed. The form is the view of the confirmation data controller.
  3. The business rule will execute when the Quantity to Sell is confirmed. The rule will reduce the UnitsInStock value of the product by the specified quantity.

Both Touch UI and RESTful API of the backend application will make the new feature known and available to their consumers. Humans will see the Sell button and the prompt to enter the Quantity to Sell. Applications will have access to the sell hypermedia control advertising the new functionality in the products resource singletons.

Start the App Builder, select the RESTful Backend Application project, choose Design, and activate the Controllers tab in the Project Explorer. Follow the instructions to add one action, one confirmation controller, and one business rule to the application.

Action

This is the sell action in the Products / Actions / ag2 (Form) action group:

Action "sell" has the non-standard ID, which makes it available in the RESTful API hypermedia. It triggers the "Custom" command with the "Sell" argument. Actions placed in the group with the "Form" scope are rendered as buttons.
Action "sell" has the non-standard ID, which makes it available in the RESTful API hypermedia. It triggers the "Custom" command with the "Sell" argument. Actions placed in the group with the "Form" scope are rendered as buttons.

The action is rendered as the Sell button in the editForm1 view when a product is selected.

Action "sell" is presented in the "editForm1" as the "Sell" button when a product is selected or edited. The reading pane renders its buttons at the top, while the modal forms will display the buttons at the bottom.
Action "sell" is presented in the "editForm1" as the "Sell" button when a product is selected or edited. The reading pane renders its buttons at the top, while the modal forms will display the buttons at the bottom.

Instructions to configure the action:

  • Expand the Products / Actions / ag2 (Form) node, right-click, and choose the New Action option.
  • Set the Command Name to Custom, enter Sell in the Command Argument, and enter Sell in the Header Text field.
  • Set its When Last Command Name to Any. This will make the action visible regardless of what command was executed last by the state machine of Touch UI.
  • Set When Key Selected to Yes. This will make action available only when a data item is selected. The action will be included in the hypermedia of the singleton resources only.
  • Enter _controller=SellPrompt in the Confirmation property. This will force the framework to create an instance of the specified data controller, which will display its first view to the user. Note that If there is no _controller instruction, then the entire text of Confirmation is displayed in the standard OK / Cancel prompt. RESTful API engine will use the specified confirmation controller to validate the action arguments.
  • Press OK to save the new action.
  • Right-click the Products / Actions / ag2 (Form) / a100 action node and choose Rename. Type in sell and press Enter to rename the node. Actions are included in the hypermedia of the corresponding resource if their identifiers do not start with the letter “a” followed by a number.
  • Drag the action node in front of the a1 to ensure that it is rendered in the first position of its scope.
Multiple actions can trigger the same command. Scope determines the Touch UI element that will trigger the action. Actions may render as form buttons, context menu options, action bar items, etc.

Confirmation Data Controller

This is the confirmation data controller SellPrompt.

The data controller without a command can be used as the action confirmation controller. It has a virtual empty row that serves as the source of data for the fields. The first controller view is displayed with the "New" command by default.
The data controller without a command can be used as the action confirmation controller. It has a virtual empty row that serves as the source of data for the fields. The first controller view is displayed with the "New" command by default.

The form1 view of the controller is displayed when the Sell button is clicked in the product form or when the Sell option is selected in its “more” menu. Users choose the number of units to sell from the radio button list. The Enter Quantity field is displayed if the Other radio button is selected.

The "SellPrompt" controller presents a view with the fields "QuantityMenu" and "Quantity" . The first field is always visible. The second field is visible only when the "Other" radio button is selected.
The "SellPrompt" controller presents a view with the fields "QuantityMenu" and "Quantity" . The first field is always visible. The second field is visible only when the "Other" radio button is selected.

Instructions to configure the confirmation data controller:

  • Click the New Controller button on the toolbar of the Project Explorer.
  • Enter SellPrompt in the Name field and save the controller.
  • Expand the new controller node and right-click on its fields node.
  • Choose the New Field option.
    • Enter QuantityMenu in the Name field.
    • Clear the Allow null values field.
    • Select the Causes Calculate checkbox. The Calculate command will trigger when users change the field value.
    • Set its Items Style to Radio Button List.
    • Press OK to save the new field.
    • Right-click the new field and select New Value/Text Item option.
      • Enter 1 in the Value field.
      • Enter One in the Text field.
      • Save the new item.
    • Add three more static items:
      • Value =5, Text = Five
      • Value = 10, Text = Ten
      • Value = Other, Text = Other
  • Right-click the SellPrompt / Fields node and choose the New Field option.
    • Enter Quantity in the Name field.
    • Set Type to Int32.
    • Clear the Allow null values field.
    • Press OK to save the new field.
  • Right-click the SellPrompt / Views node and choose the New View option.
    • Enter form1 in the Name field.
    • Set Type to Form.
    • Enter the following tag names separated by space in the Tags field:
      • Tag modal-always will make the view to always appear as a modal popup.
      • Tag modal-fit-content will force the view to adjust its height to fit the contents.
      • Tag modal-max-xs will not let the view width to be wider than the extra-small.
      • Tag modal-auto-grow will make the view grow its content when the previously hidden fields become visible.
      • Tag material-icon-sell will display the “sell” material icon in the view header.
    • Enter Quantity to Sell in the Label field.
    • Press OK to save the new view.
  • Ctrl-click the SellPrompt / Fields / QuantityMenu and SellPrompt / Fields / Quantity field nodes.
  • Drag the selected fields onto the SellPrompt / Views / form1 view node.
  • Double-click the SellPrompt / Views / form1 / QuantityMenu data field.
    • Enter lookup-auto-advance tag name in the Tags field. The focus will shift from the tagged data field to the next input when its value is changed. The QuantityMenu data field is the only visible field in the form until its value is set to Other option. The selection of the Other option will reveal the Quantity field. The focus will automatically advance from the QuantityMenu static lookup to Quantity.
    • Enter $blank in the Header Text field. The data field will have no label when rendered.
    • Save the changes.
  • Double-click the SellPrompt / Views / form1 / Quantity field.
    • Enter $row.QuantityMenu == 'Other' in the Visible When field. This JavaScript expression is evaluated whenever the field values are changed. The variable $row represents the object with the properties set to values entered in the form inputs.
    • Save the changes.
  • Right-click the SellPrompt / Business Rules node and choose the New Business Rule option.
    • Set Type to SQL.
    • Enter Calculate in the Command Name field.
    • Set Phase to Execute.
    • Paste the text of the SQL business rule shown below into the Script field.
    • Save the new business rule.

This is the script of the SellPrompt / Business Rules / r100 rule:

SQL
1234if @QuantityMenu != 'Other'
    set @Quantity = @QuantityMenu
else
    set @Quantity = null

The business rule will copy the value of the QuantityMenu field to the Quantity if the value of the former is not equal to the Other constant. Otherwise the Quantity value is cleared. The business rule will execute during the roundtrip initiated by Touch UI when the radio button option is selected in the menu. The Quantity field value is returned from the server to the client and copied into the corresponding input control. Field values entered in the form are referenced as parameters in the anonymous SQL script of the business rule. The script is written in the programming language of the database engine. Transact-SQL is the language of Microsoft SQL Server.

Developers familiar with the client-side programming can avoid the server round trip with the business rule of the JavaScript type. The final call of the preventDefault method in the script is telling Touch UI to stop the default execution flow.

JavaScript
12345if ($row.QuantityMenu !== 'Other')
    $row.Quantity = $row.QuantityMenu;
else 
    $row.Quantity = null;
this.preventDefault();
Data controllers are the building blocks of applications created with Code On Time. The instance of a controller with a command will present a view with the data fetched after its execution. The controller without a command presents a view of an empty row in the “New” mode.

Business Rule

The SQL business rule Products / Business Rules / r100 executes on the server when the OK button is pressed in the Quantity to Sell confirmation. The execution context of the rule is the editForm1 view of the Products data controller.

Business rules of a data controller execute in the order of their declaration when their "Command", "Argument", and "View" are matched to an action. JavaScript rules execute on the client. SQL, Code, and Email rules execute on the server.
Business rules of a data controller execute in the order of their declaration when their "Command", "Argument", and "View" are matched to an action. JavaScript rules execute on the client. SQL, Code, and Email rules execute on the server.

This is the life-cycle of the sell action, SellPrompt confirmation controller, and Products / Business Rules / r100 rule:

  • The action execution flow starts with the confirmation displayed to collect the parameters in the SellPrompt.
  • User enters the quantity to sell and confirms the prompt.
  • The focus returns to the product form.
  • The framework executes the client-side business rules first and passes the action to the server-side code if the default execution flow has not been prevented on the client.
  • The server-side framework searches for the rules to execute the sell action. The definition of the business rule r100 matches the command name and argument of the action. The rule is invoked by the framework.
  • The rule manipulates the action prompt values via the parameters with the @Parameters_ prefix.
  • The server-side framework returns the result of action processing to the client.
  • If an error has been raised or a client-side script has been emitted by the server-side code, then the default user interface flow stops - the active form remains visible, the error is displayed, and the emitted JavaScript is executed.
  • Touch UI navigates the user back to the previous view if there are no errors to display or scripts to execute.

The r100 rule implementation retrieves the UnitsInStock column value from the Products table. If the value of the @Parameters_Quantity is less than the number of available units, then the quantity of available units is reduced. Otherwise the error message is returned.

SQL
123456789101112-- figure the stock in the database
declare @InStock int
select @InStock = UnitsInStock from Products where ProductID = @ProductID
-- decrease the stock if available 
if (@InStock >= @Parameters_Quantity)
    update Products
    set UnitsInStock = UnitsInStock- @Parameters_Quantity
    where ProductID = @ProductID
else
    set @Result_Error = 
        'Not enough quantity in stock to sell ' + 
        cast(@Parameters_Quantity as varchar) + ' units!'

This is the example of the error message when an attempt to sell too many units is detected. The product form remains open.

Errors raised by business rules are presented as notifications above the active view. The reading pane in the screenshot shows the "editForm1" view. The form has been active when the "Sell" action was initiated.
Errors raised by business rules are presented as notifications above the active view. The reading pane in the screenshot shows the "editForm1" view. The form has been active when the "Sell" action was initiated.

Instructions to configure the business rule:

  • Right click the Products / Business Rules node and choose the New Business Rule option.
  • Set Type to SQL.
  • Enter Custom in the Command Name field.
  • Enter Sell in the Command Argument field.
  • Set Phase to Execute.
  • Paste the text of the SQL business rule shown above into the Script field.
  • Save the new business rule.
Business rules are triggered when their command and argument are matched with the custom or built-in action. The rules can be written in JavaScript, SQL, and Code (C# or VB).

Trying it Out

Generate the customized app and try out the sell action in the user interface. The value of the Units In Stock field of the sold product must be reduced after each sale. An error will be raised whenever an attempt to sell too many units is made.

Note that the action, the prompt, and the business rule will operate on the Products page and on the Suppliers page. The former provides direct access to products. The latter requires selecting a product associated with a supplier.

The sell action is also available in the hypermedia of the RESTful API. Touch UI Frontend and RESTful API Engine are providing an interpretation of the data controllers for their respective audiences.

Custom Actions in Singletons

The audience of the RESTful API Engine is represented by the client applications and software developers. The hypermedia is their primary interface.

The hypermedia control of the sell action can be found in the resource singletons of products. Take a look at the following singleton resource sample. The href property of the control is set to /v2/products/19/sell and the method property is set to POST. The number 19 in href is the primary key of the product.

JSON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263{
    "_links": {
        "self": {
            "href": "/v2/products/19"
        },
        "up": {
            "href": "/v2/products"
        },
        "collection": {
            "href": "/v2/products?count=true"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5"
        },
        "supplierId": {
            "href": "/v2/products/19/supplier-id",
            "embeddable": true
        },
        "lookup-supplierId": {
            "href": "/v2/suppliers?count=true&fields=supplierId,companyName",
            "embeddable": true
        },
        "categoryId": {
            "href": "/v2/products/19/category-id",
            "embeddable": true
        },
        "lookup-categoryId": {
            "href": "/v2/categories?count=true&fields=categoryId,categoryName",
            "embeddable": true
        },
        "edit": {
            "href": "/v2/products/19",
            "method": "PATCH"
        },
        "replace": {
            "href": "/v2/products/19",
            "method": "PUT"
        },
        "delete": {
            "href": "/v2/products/19",
            "method": "DELETE"
        },
        "sell": {
            "href": "/v2/products/19/sell",
            "method": "POST"
        },
        "schema": {
            "href": "/v2/products/19?_schema=true"
        }
    },
    "productId": 19,
    "productName": "Teatime Chocolate Biscuits",
    "supplierId": 8,
    "supplierCompanyName": "Specialty Biscuits, Ltd.",
    "categoryId": 3,
    "categoryName": "Confections",
    "quantityPerUnit": "10 boxes x 12 pieces",
    "unitPrice": 9.2000,
    "unitsInStock": 19,
    "unitsOnOrder": 0,
    "reorderLevel": 5,
    "discontinued": false
}

Use Postman to execute an authenticated POST request without a body to the URL specified by the sell hypermedia control. The response will include the error message and the resource schema. The error is reported since the required parameters are missing. The schema explains the expected request input and response output.

JSON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101{
    "error": {
        "errors": [
            {
                "id": "77102da4-3bfb-480c-8168-2c143f7682b4",
                "reason": "invalid_argument",
                "message": "Field 'parameters' is expected in the body."
            }
        ],
        "code": 400,
        "message": "Bad Request"
    },
    "_schema": {
        "_input": {
            "_schema": {
                "parameters": {
                    "required": true,
                    "_schema": {
                        "quantityMenu": {
                            "type": "String",
                            "required": true,
                            "label": "$blank"
                        },
                        "quantity": {
                            "type": "Int32",
                            "required": true,
                            "label": "Enter Quantity"
                        }
                    }
                },
                "*": true
            }
        },
        "productId": {
            "type": "Int32",
            "required": true,
            "key": true,
            "readOnly": true,
            "label": "Product ID"
        },
        "productName": {
            "type": "String",
            "length": 40,
            "required": true,
            "label": "Product Name"
        },
        "supplierId": {
            "type": "Int32",
            "lookup": true,
            "label": "Supplier ID"
        },
        "supplierCompanyName": {
            "type": "String",
            "length": 40,
            "readOnly": true,
            "label": "Supplier Company Name"
        },
        "categoryId": {
            "type": "Int32",
            "lookup": true,
            "label": "Category ID"
        },
        "categoryName": {
            "type": "String",
            "length": 15,
            "readOnly": true,
            "label": "Category Name"
        },
        "quantityPerUnit": {
            "type": "String",
            "length": 20,
            "label": "Quantity Per Unit"
        },
        "unitPrice": {
            "type": "Decimal",
            "default": "((0))",
            "label": "Unit Price"
        },
        "unitsInStock": {
            "type": "Int16",
            "default": "((0))",
            "label": "Units In Stock"
        },
        "unitsOnOrder": {
            "type": "Int16",
            "default": "((0))",
            "label": "Units On Order"
        },
        "reorderLevel": {
            "type": "Int16",
            "default": "((0))",
            "label": "Reorder Level"
        },
        "discontinued": {
            "type": "Boolean",
            "default": "((0))",
            "required": true,
            "label": "Discontinued"
        }
    }
}

If the action has a confirmation, then the parameters field is expected. The _schema.input._schema key makes the engine expect the required parameters field in the request body with the quantityMenu and quantity fields designated inside. These are the fields of the SellPrompt action confirmation controller. The _schema.input._schema.* key is set to true to signify that the body of the request may also include any field of the resource.

The body of the custom action request can include any number of fields of the resource described in the _schema key. For example, the body may include the productName, reorderLevel, and unitsInStock fields. The engine automatically fetchs the singleton resource and treats the field values of the request as the “new” field values when the processing of the action begins. The implementation of a custom action can interpret the “old” and “new” values of the resource fields as needed. There is no default processing.

This the valid request body according to the schema:

JSON
123456{
    "parameters": {
        "quantityMenu": "any text",
        "quantity": 1
    }
}

Post this payload to the URL of the sell action. Repeat the request a few times and observe that the quantityInStock field of the resource is reduced by 1 in each instance.

Custom action of a singleton may have the payload with custom "parameters" along with the optional resource fields. Parameters are automatically derived from the action confirmation controller. The optional resource fields in the request body are the "new" values of the singleton fields.
Custom action of a singleton may have the payload with custom "parameters" along with the optional resource fields. Parameters are automatically derived from the action confirmation controller. The optional resource fields in the request body are the "new" values of the singleton fields.

The utility field QuantityMenu has been introduced to the SellPrompt controller with the sole purpose of speeding up the input of the Quantity field. The Products / Business Rules / r100 rule is making use of the @Parameters_Quantity parameter only and ignores the QuantityMenu field.

The field parameters.quantityMenu is clearly redundant in the body of the request. How do we remove the unused quantityMenu field from the RESTful API while retaining its function in Touch UI?

The rest-api-none tag can be applied to views and data fields to make them invisible in the RESTful API.

Select the backend application project on the start page in Code On Time app builder. Choose Design, and activate the Controllers tab. Select the SellPrompt / Views / form1 / c1 / quantityMenu data field node. Add rest-api-none tag to the Tags and save changes. Generate the application.

Try the POST request to the sell action URL. The engine will let you know that the quantityMenu is not expected anymore

JSON
12345678910111213{
    "error": {
        "errors": [
            {
                "id": "0076be9b-f66a-4c31-a19e-f58ead30cded",
                "reason": "invalid_argument",
                "message": "Unexpected field 'parameters.quantityMenu' is specified in the body."
            }
        ],
        "code": 400,
        "message": "Bad Request"
    }
}

Remove the field quantityMenu from the parameters in the request body. The sell action will execute without errors. The field will also disappear from the sell resource schema.

Custom Actions in Collections

The custom action discussed above is available in the hypemedia of the singleton resources only, since its When Key Selected property is set to Yes. Actions that do not require a selection of an item will be available in the collection resources.

Follow the instructions to configure another custom action:

  • Right-click the Products / Actions / ag3 (ActionBar) - New action group and choose the New Action context menu option.
  • Set the Command Name to Custom.
  • Enter DoSomething in the Command Argument.
  • Enter Do Something in the Header Text.
  • Set When Key Selected to No.
  • Enter material-icon-archive in the Icon / Custom Style.
  • Save the action.
  • Right-click the new action node, choose Rename, and change the rule ID to doSomething.

Here is how the action is presented in Touch UI after the app has been generated. The action is visible in the action bar and in the system context menu whether or not a product is selected. Nothing happens when the action executes, since there are no business rules to handle the action.

System context menu slides out on the right side of the screen when the "More" button is pressed in Touch UI apps. All actions available to the user in the given context are listed in the menu. Action "Do Something" can be activated from the context menu or the page toolbar.
System context menu slides out on the right side of the screen when the "More" button is pressed in Touch UI apps. All actions available to the user in the given context are listed in the menu. Action "Do Something" can be activated from the context menu or the page toolbar.

The interface of apps and developers has also changed. The hypermedia of the products collection resource contains the do-something hypermedia control.

JSON
12345678910111213141516171819202122232425262728293031{
    "_links": {
        "self": {
            "href": "/v2/products"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5"
        },
        "lookup-supplierId": {
            "href": "/v2/suppliers?count=true&fields=supplierId,companyName",
            "embeddable": true
        },
        "lookup-categoryId": {
            "href": "/v2/categories?count=true&fields=categoryId,categoryName",
            "embeddable": true
        },
        "create": {
            "href": "/v2/products",
            "method": "POST"
        },
        "do-something": {
            "href": "/v2/products/do-something",
            "method": "POST"
        },
        "schema": {
            "href": "/v2/products?_schema=true"
        }
    },
    "collection": [
    ]
}

The action is not advertised in the hypermedia of the singleton resources.

Actions with the unspecified value in the When Key Selected property are available in the hypermedia of both singletons and collections.

Try posting to the URI of the do-something hypermedia control. The response will have the collection hypermedia links and nothing else.

JSON
12345678910111213{
    "_links": {
        "up": {
            "href": "/v2/products?count=true"
        },
        "collection": {
            "href": "/v2/products?count=true"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5"
        }
    }
}

If the x-restful-schema request header is set to only, then the schema will be included in the response. The framework will not attempt to execute the action.

JSON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798{
    "_schema": {
        "_input": {
            "_schema": {
                "parameters": {
                    "_schema": {}
                },
                "collection": {
                    "array": true,
                    "_schema": {
                        "productId": {
                            "type": "Int32",
                            "required": true
                        }
                    }
                },
                "*": true
            }
        },
        "productId": {
            "type": "Int32",
            "required": true,
            "key": true,
            "readOnly": true,
            "label": "Product ID"
        },
        "productName": {
            "type": "String",
            "length": 40,
            "required": true,
            "label": "Product Name"
        },
        "supplierId": {
            "type": "Int32",
            "lookup": true,
            "label": "Supplier ID"
        },
        "supplierCompanyName": {
            "type": "String",
            "length": 40,
            "readOnly": true,
            "label": "Supplier Company Name"
        },
        "categoryId": {
            "type": "Int32",
            "lookup": true,
            "label": "Category ID"
        },
        "categoryName": {
            "type": "String",
            "length": 15,
            "readOnly": true,
            "label": "Category Name"
        },
        "quantityPerUnit": {
            "type": "String",
            "length": 20,
            "label": "Quantity Per Unit"
        },
        "unitPrice": {
            "type": "Decimal",
            "default": "((0))",
            "label": "Unit Price"
        },
        "unitsInStock": {
            "type": "Int16",
            "default": "((0))",
            "label": "Units In Stock"
        },
        "unitsOnOrder": {
            "type": "Int16",
            "default": "((0))",
            "label": "Units On Order"
        },
        "reorderLevel": {
            "type": "Int16",
            "default": "((0))",
            "label": "Reorder Level"
        },
        "discontinued": {
            "type": "Boolean",
            "default": "((0))",
            "required": true,
            "label": "Discontinued"
        }
    },
    "_links": {
        "up": {
            "href": "/v2/products?count=true&_schema=only"
        },
        "collection": {
            "href": "/v2/products?count=true&_schema=only"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5&_schema=only"
        }
    }
}

The schema of the collection custom action is similar to the schema of the singleton custom action. Optional parameters and field names of the resource may be defined in the request body. There is also the optional collection key that must be specified as an array of objects with the primary key field values. If the collection is not specified or empty, then the server-side framework will execute the matched business rules one time only. If the collection is specified, then the framework will execute the business rules once for each item.

The following SQL business rule will raise an error if the collection of primary keys is not specified. Otherwise it will increase the UnitPrice of each product in the collection by 1 cent.

SQL
123456if @ProductID is null
    set @Result_Error = 'A collection of product identifiers is expected.'
else
    update Products
      set UnitPrice = UnitPrice + 0.01
      where ProductID = @ProductID

Add the rule to the Products data controller in the Project Designer. Make sure that the Command Name is set to Custom, the Command Argument is set to DoSomething, and the Phase is set to Execute. Generate the application. Post the request without a body to the URI of the do-something action. The error message will be returned.

JSON
12345678910111213{
    "error": {
        "errors": [
            {
                "id": "3ac6f887-0953-4743-b6bc-9a09c0d84e85",
                "reason": "method_rejected",
                "message": "A collection of product identifiers is expected."
            }
        ],
        "code": 500,
        "message": "Internal Server Error"
    }
}

The same request with the following body will execute with no errors and change the UnitPrice of the products specified in the collection array.

JSON
12345678910111213{
  "collection": [
    {
      "productId": 1
    },
    {
      "productId": 17
    },
    {
      "productId": 35
    }
  ]
}

Reporting Actions

Actions may return binary data in the response. The server-side code of the application framework has the built-in processing for the Report command. It creates a dynamic RDLC report from the data controller definition and produces the output in Adobe PDF, Microsoft Word, Microsoft Excel, or TIFF format with the help of the Microsoft Report Viewer.

Touch UI fetches the output of "Report" actions and forces the browser to display the "Download" prompt. The native or installed viewers are required on the client to view the reports.
Touch UI fetches the output of "Report" actions and forces the browser to display the "Download" prompt. The native or installed viewers are required on the client to view the reports.

Developers have an option to create the custom reports in RDLC format for the specific data controller views. Custom “Code” business rules may override the default processing and return any type of binary data in the response to the Report action. The third-party tools can be engaged to produce the output.

RESTful API engine will advertise an action with the Report command name and the non-standard identifier in the resource hypermedia. Reporting actions are configured with no value in the When Key Selected property. This makes them available in the hypermedia of both collections and singletons.

Rename the Products / Actions / ag7 / a1 action node to report1 in the Project Designer.

image3.png

Generate the app and locate the report1 hypermedia control in the products collection.

JSON
12345678910111213141516171819202122232425262728293031323334{
    "_links": {
        "self": {
            "href": "/v2/products"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5"
        },
        "lookup-supplierId": {
            "href": "/v2/suppliers?count=true&fields=supplierId,companyName",
            "embeddable": true
        },
        "lookup-categoryId": {
            "href": "/v2/categories?count=true&fields=categoryId,categoryName",
            "embeddable": true
        },
        "create": {
            "href": "/v2/products",
            "method": "POST"
        },
        "do-something": {
            "href": "/v2/products/do-something",
            "method": "POST"
        },
        "report1": {
            "href": "/v2/products/report1"
        },
        "schema": {
            "href": "/v2/products?_schema=true"
        }
    },
    "collection": [
    ]
}

Note that the HTTP method is not specified in the report1 control. Follow the link in Postman and execute an authenticated GET request. The request will fetch the four pages of the PDF document produced by the action.

All items of a collection are included in the report output by default.
All items of a collection are included in the report output by default.

Set the filter parameter of the request to unitPrice > 50 expression. Set the sort parameter to UnitPrice desc value. Both parameters will appear in the URL. Execute the request. One page report of products priced over $50 and sorted in the descending order of the Unit Price will be returned.

Parameters "filter" and "sort" specified in the URL will change the number of items and sort order in the report. If the "Report" action is executed with the POST method then the body of the request may include "parameters" and "collection" fields.
Parameters "filter" and "sort" specified in the URL will change the number of items and sort order in the report. If the "Report" action is executed with the POST method then the body of the request may include "parameters" and "collection" fields.

The same output will be produced if the request method is changed to POST. The reporting actions can be executed with either GET or POST method.

Reporting actions can have their own parameters and include only a specific set of collection items identified by their primary keys. Execute the request with the POST method to specify the parameters and collection fields in the request body.

Action Result

A custom action executed on a singleton will return the resource itself in the response. A custom action executed on a collection will return the hypermedia only. Developers may provide a custom action result for either resource type.

Returning Simple Values

Change the script of the SQL business rule responding to the Custom / DoSomething action in the Products controller as follows:

SQL
1234set @Result_ReturnValue = getdate()
set @UnitPrice = 9.99
set @UnitsInStock = 10
set @ReorderLevel = 5

The script manipulates the resource fields and the special Result_ReturnValue parameter. Execute the POST request to the URL of the do-something hypermedia control. The corresponding values will appear in the response.

JSON
12345678910111213141516171819{
    "_links": {
        "up": {
            "href": "/v2/products?count=true"
        },
        "collection": {
            "href": "/v2/products?count=true"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5"
        }
    },
    "result": {
        "returnValue": "2022-04-25T18:59:00.0000000",
        "unitPrice": 9.9900000000,
        "unitsInStock": 10,
        "reorderLevel": 5
    }
}

Returning Result Set

If the resource field names and returnValue are not enough to present the action result, then consider producing a result set. The script must set the BusinessRules_EnableResultSet system parameter to 1. This will prepare the framework to expect a rowset to be in the output of the rule.

Change rule script of the do-something action like this:

SQL
12set @BusinessRules_EnableResultSet = 1
select count(*) CountOfShippers, getdate() 'Today' from shippers

The response will include the custom fields countOfShippers and today in the result key.

JSON
1234567891011121314151617{
    "_links": {
        "up": {
            "href": "/v2/products?count=true"
        },
        "collection": {
            "href": "/v2/products?count=true"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5"
        }
    },
    "result": {
        "countOfShippers": 3,
        "today": "2022-04-25T19:17:29.5570000"
    }
}

Note that the SELECT statement in the rule does not have the FROM clause. Microsoft SQL Server creates a virtual rowset with a single row in this instance. If only one row is returned, then the fields will be embedded directly into the result key.


Change the SELECT statement in the script to select * from shippers. The result key will have the collection of items.

JSON
1234567891011121314151617181920212223242526272829303132{
    "_links": {
        "up": {
            "href": "/v2/products?count=true"
        },
        "collection": {
            "href": "/v2/products?count=true"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5"
        }
    },
    "result": {
        "collection": [
            {
                "shipperId": 1,
                "companyName": "Speedy Express",
                "phone": "(503) 555-9831"
            },
            {
                "shipperId": 2,
                "companyName": "United Package",
                "phone": "(503) 555-3199"
            },
            {
                "shipperId": 3,
                "companyName": "Federal Shipping",
                "phone": "(503) 555-9931"
            }
        ]
    }
}

The column names in the rowset produced by the rule represent a subset of the collection resource field names, then the result is replaced with the collection key in the response body.

Enter the following as the script of the same business rule and generate the app.

SQL
123456set @BusinessRules_EnableResultSet = 1
select top 3 
    ProductId, 
    ProductName, 
    UnitPrice
from Products

The response to the do-something action will have the collection key instead of the result. The missing fields of the collection resource are included explicitly with the null values.

JSON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657{
    "_links": {
        "up": {
            "href": "/v2/products?count=true"
        },
        "collection": {
            "href": "/v2/products?count=true"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5"
        }
    },
    "collection": [
        {
            "productId": 1,
            "productName": "Chai",
            "unitPrice": 18.0500,
            "categoryId": null,
            "categoryName": null,
            "discontinued": null,
            "quantityPerUnit": null,
            "reorderLevel": null,
            "supplierCompanyName": null,
            "supplierId": null,
            "unitsInStock": null,
            "unitsOnOrder": null
        },
        {
            "productId": 2,
            "productName": "Chang",
            "unitPrice": 19.0000,
            "categoryId": null,
            "categoryName": null,
            "discontinued": null,
            "quantityPerUnit": null,
            "reorderLevel": null,
            "supplierCompanyName": null,
            "supplierId": null,
            "unitsInStock": null,
            "unitsOnOrder": null
        },
        {
            "productId": 3,
            "productName": "Aniseed Syrup",
            "unitPrice": 10.0400,
            "categoryId": null,
            "categoryName": null,
            "discontinued": null,
            "quantityPerUnit": null,
            "reorderLevel": null,
            "supplierCompanyName": null,
            "supplierId": null,
            "unitsInStock": null,
            "unitsOnOrder": null
        }
    ]
}

Invoking Custom Actions in Client Apps

The CRUD with Hypermedia tutorial explains how to build a plain JavaScript client for the RESTful API Engine of the backend application. It provides a perfect playground to demonstrate the custom actions sell and report1.

Extend the renderProductData function to add the Report button to the product list toolbar. The code snippet of the modified function shows the definition of the button with the yellow background. The button will be enabled if there is the report1 hypermedia in the product list data. The action is advertised in the hypermedia of the products collection resource.

JavaScript
12345678910111213141516171819202122232425262728293031323334function renderProductData() {
    var sb = [
        '<table border="1" cellpadding="5" cellspacing="0">',
        '<tr><th>Product</th><th>Category</th><th>Supplier</th><th>Unit Price</th><th>Units In Stock</th></tr>'];
    for (var i = 0; i < products.collection.length; i++) {
        var p = products.collection[i];
        sb.push('<tr data-index="', i, '">');
        sb.push('<td>', htmlEncode(p.productName), '</td>');
        sb.push('<td>', htmlEncode(p.categoryName), '</td>');
        sb.push('<td>', htmlEncode(p.supplierCompanyName), '</td>');
        sb.push('<td>', htmlEncode(p.unitPrice), '</td>');
        sb.push('<td>', htmlEncode(p.unitsInStock), '</td>');
        sb.push('</tr>')
    }
    sb.push('</table>');
    sb.push('<div class="toolbar">')
    var hypermedia = products._links;
    sb.push('<button data-hypermedia="new"', hypermedia.create ? '' : ' disabled="disabled"', '>New</button>');
    // *************************************************************************
    // The "Report" button with the yellow background
    sb.push('<button data-hypermedia="report" style="background-color:yellow"',
        hypermedia.report1 ? '' : ' disabled="disabled"', '>Report</button>');
    // *************************************************************************
    sb.push('<button data-hypermedia="self"', hypermedia.self ? '' : ' disabled="disabled"', '>Refresh</button>');
    sb.push('<button data-hypermedia="first"', hypermedia.first ? '' : ' disabled="disabled"', '>First</button>');
    sb.push('<button data-hypermedia="prev"', hypermedia.prev ? '' : ' disabled="disabled"', '>Prev</button>');
    sb.push('<button data-hypermedia="next"', hypermedia.next ? '' : ' disabled="disabled"', '>Next</button>');
    sb.push('<button data-hypermedia="last"', hypermedia.last ? '' : ' disabled="disabled"', '>Last</button>');
    var hypermediaArgs = urlArgs(products._links.self);
    sb.push('Page: ', parseInt(hypermediaArgs.page) + 1, ' of ', Math.ceil(products.count / hypermediaArgs.limit),
        ' (', + products.count + ' items)');
    sb.push('</div>');
    document.querySelector('#product-list').innerHTML = sb.join('');
}

The Report button appears right next to the New button in the pager toolbar. A browser prompt to save the PDF file of the report will appear when the button is clicked.

The system browser prompt is displayed when the "Report" button is clicked. Client apps can process the JSON, XML, and YAML responses from the server. The application engine can also stream out the binary data, such as PDF reports or images.
The system browser prompt is displayed when the "Report" button is clicked. Client apps can process the JSON, XML, and YAML responses from the server. The application engine can also stream out the binary data, such as PDF reports or images.

Change the readProduct function to add the Sell button to the Edit Product form. The button will appear next to the Update button if there is the sell hypermedia link in the obj instance of the fetched product.

JavaScript
12345678910111213141516171819function readProduct(selectedIndex) {
    $app.restful({
        url: products.collection[selectedIndex]._links.self
    }).then(obj => {
        showForm({
            title: 'Edit Product',
            from: obj,
            buttons: [
                { text: 'Update', hypermedia: 'edit' },
                { text: 'Sell', hypermedia: 'sell' }, // the "Sell" button definition
                { text: 'Delete', hypermedia: 'delete' }
            ]
        }).then(form => {
            form.fields.forEach(f => {
                form.elem.querySelector('[data-field="' + f.name + '"]').value = obj[f.name];
            });
        });
    })
}

The browser confirmation will appear when the button is clicked.

A click on the "Sell" button will execute a sale of one product unit.  If the sale has failed, then the product form will remain visible and an alert with the error message will be displayed.
A click on the "Sell" button will execute a sale of one product unit. If the sale has failed, then the product form will remain visible and an alert with the error message will be displayed.

Finally, modify the handleHypermedia function to include the implementation of the sell and report actions.

JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061function handleHypermedia(e) {
    var row = e.target.closest('[data-index]')
    if (row && products)
        readProduct(row.getAttribute("data-index"));
    else {
        var btn = e.target.closest('[data-hypermedia]');
        if (btn && products) {
            var hypermedia = btn.getAttribute('data-hypermedia');
            switch (hypermedia) {
                case 'sell':
                    // invoke custom action "sell"
                    if (confirm('Sell?'))
                        $app.restful({
                            url: showForm.sell,
                            body: {
                                "parameters": {
                                    "quantity": 1
                                }
                            }
                        })
                            .then(() => {
                                hideForm();
                                refreshProductList(products._links.self);
                            })
                            .catch(restfulException);
                    break;
                case 'report':
                    // invoke action "report1"
                    $app.restful({
                        url: products._links.report1,
                    }).then(blob => {
                        let dataUrl = URL.createObjectURL(blob);
                        let a = document.createElement('a');
                        a.setAttribute('download', blob.name);
                        a.setAttribute('href', dataUrl);
                        a.click();
                        URL.revokeObjectURL(dataUrl);
                    });
                    break;
                case 'new':
                    newProduct();
                    break;
                case 'create':
                    postProduct();
                    break;
                case 'edit':
                    patchProduct();
                    break;
                case 'delete':
                    deleteProduct();
                    break;
                case 'cancel':
                    hideForm();
                    break;
                default:
                    refreshProductList(products._links[hypermedia]);
                    break;
            }
        }
    }
}

The function will respond to the click on the Sell button by invoking the $app.restful method with the url argument set to the hypermedia control of the sell action. The control is the property of the showForm function used to display the selected product item. The body argument specifies the custom action parameters with the quantity of 1. The implementation will “sell” a single product unit only. The form is closed if the product was sold successfully and the current page of the product list is refreshed.

The function will respond to the click on the Report button by invoking the $app.restful method with the url argument set to the report1 hypermedia control found in the product list data. If the response content type is not JSON, XML, or Yaml, then the $app.restful method will convert the response data into a Blob object. The then chain function expects the binary content and creates an Object URL from the blob in the argument. The URL is assigned to a dynamically created link element. The link is clicked programmatically to cause the Save File prompt of the browser to appear.

Next

Once in a while the client apps will need to submit the binary data to the backend application or have it fetched and presented to users. Multiple fields of the Binary Large Object type may be a part of a resource. Learn how to specify the BLOBs as parameters of the $app.restful method and in the raw HTTP requests.

Continue to RESTful Workshop