Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[SAPUI5] Add custom header parameter to all oData queries

Simply go to your Component.js file and add this line to the init function:

this.getModel().setHeaders({"myCustomParameter": "test"})

In a CAP Backend, you get this parameter from the express req object, which can be accessed via the req.http property:

req.http.req.headers['myCustomParameter']

And here is a nice code snippet, on how to read this header parameter in an ABAP system: https://answers.sap.com/answers/425621/view.html

[SAPUI5] Suppress default oData error message / display meaningful error messages

Create a separate ErrorHandler.js file, like it is described here and either do your own error handler implementation, or take the sample from here. To avoid displaying multiple errors at once, follow this chapter.

Thanks to the provided dsag sample, this is takes only a few minutes and improves the user experience a lot!

[SuccessFactors] OData V2 – filter in (VSC Rest-API Client)

When filtering an OData V2 endpoint, you can simply list your values separated by a comma after the keyword in (option 2). Much shorter than having to repeat your filter statement all the time, like in option 1.

@user1=10010
@user2=10020

### Option 1: Filter userId using OR condition
GET {{$dotenv api_url}}/odata/v2/User?$filter=userId eq '{{user1}}' or userId eq '{{user2}}'
Authorization: Basic {{$dotenv api_auth}}
Accept: application/json

### Option 2: Filter userId using IN condition
GET {{$dotenv api_url}}/odata/v2/User?$filter=userId in '{{user1}}', '{{user2}}'
Authorization: Basic {{$dotenv api_auth}}
Accept: application/json

[ABAP] OData – GET_STREAM implementation to return a PDF

  METHOD /iwbep/if_mgw_appl_srv_runtime~get_stream.

* This method get's called when a media file is queried with $value. A binary stream will be returned.

    TRY.
        DATA(file_id) = VALUE zfile_id( it_key_tab[ name = 'file_id' ]-value ).
      CATCH cx_sy_itab_line_not_found.
        RETURN. " leave here when no file_id provided
    ENDTRY.
  
    DATA(ls_file) = get_file( file_id ) " read your file you want to return (if it's not yet a binary stream, convert it)

    DATA(ls_stream) = VALUE ty_s_media_resource( value     = ls_file-value
                                                 mime_type = ls_file-mimetype ). " in my case it's 'application/pdf'

    " necessary to display the filename instead of $value in the viewer title
    TRY.
        " create pdf object
        DATA(lo_fp)     = cl_fp=>get_reference( ).
        DATA(lo_pdfobj) = lo_fp->create_pdf_object( connection = 'ADC' ).
        lo_pdfobj->set_document( pdfdata = ls_stream-value ).
        " set title
        lo_pdfobj->set_metadata( VALUE #( title = ls_file-filename ) ).
        lo_pdfobj->execute( ).
        " get pdf with title
        lo_pdfobj->get_document( IMPORTING pdfdata = ls_stream-value ).

      CATCH cx_fp_runtime_internal
            cx_fp_runtime_system
            cx_fp_runtime_usage INTO DATA(lo_fpex).
    ENDTRY.

    copy_data_to_ref( EXPORTING is_data = ls_stream
                      CHANGING  cr_data = er_stream ).

    " necessary for the pdf to be opened inline instead of a download (also sets the filename when downloaded)
    /iwbep/if_mgw_conv_srv_runtime~set_header( VALUE #( name  = 'content-disposition'
                                                        value = |inline; filename={ ls_file-filename }| ) ).

  ENDMETHOD

Quick way to open a PDFViewer in your UI5 App:

			const pdfViewer = new PDFViewer()
			pdfViewer.setSource("/sap/opu/odata/ZMY_SEVICE" + my_path + "/$value")  // my_path could be something like this "/PdfSet('file_id')"
			pdfViewer.setTitle("My PDFViewer Title") // title of the popup, not the viewer
			pdfViewer.open()

[ABAP] OData – Filtering, Sorting, Paging

    " Filter, Sort, Paging
    /iwbep/cl_mgw_data_util=>filtering( EXPORTING it_select_options = it_filter_select_options
                                        CHANGING  ct_data           = et_entityset ).
    /iwbep/cl_mgw_data_util=>orderby(   EXPORTING it_order          = it_order
                                        CHANGING  ct_data           = et_entityset ).
    /iwbep/cl_mgw_data_util=>paging(    EXPORTING is_paging         = is_paging
                                        CHANGING  ct_data           = et_entityset ).

[SAPUI5] Get and set properties of a binded model and submit changes

Get:

const oModel = this.getView().getModel()
const sPath = this.getView().getBindingContext().sPath
const sID = oModel.getProperty(sPath+"/ID")

Set:

const newID = "12345"
oModel.setProperty(sPath+"/ID", newID)

When using the set property function, you can now submit your changes this way:

        // First check if there are any changes    
        if (!oModel.hasPendingChanges()) {
           MessageToast.show("Nothing to do!")
           return
        }
        
        // Now submit your changes
        oModel.submitChanges({
          success: () => MessageToast.show("Success!"),
          error: (err) => alert(err)
        })

This way is much more comfortable, than using oModel.update().

[CAP] SAPUI5 Filter using FilterOperator.Contains with oData V2

In my SAPUI5 Freesstyle frontend I created a search field above a list. In the searchfield handler I’m creating a filter with the provided query.

const sQuery = oEvent.getParameter("query");
new Filter("firstName", FilterOperator.Contains, sQuery);

Afterwards I’m binding the filter to my list to trigger the binding refresh. But when debugging the backend handler I noticed the following…

In my CAP on Read handler, the filter gets converted into a V4 compatible filter expression:

oData V4: $filter=contains(firstName,'Max')

As I’m forwarding the request to an external V2 oData API (SuccessFactors) this would not work, as for V2 the following filter syntax is needed:

oData V2: $filter=substringof('Max',firstName) eq true

As I could not find any solution to this problem, I manually passed my filter as custom property to my CAP Service and did a manual select.

Adding the custom property in the frontend in my searchfield handler:

onSearch: function (oEvent) {
			if (oEvent.getParameters().refreshButtonPressed) {
				this.onRefresh();
				return;
			}

			let oBindingInfo = this._oList.getBindingInfo("items");
			if (!oBindingInfo.parameters) oBindingInfo.parameters = {};
			if (!oBindingInfo.parameters.custom) oBindingInfo.parameters.custom = {};

			if (oEvent.getParameter("query")) {
				oBindingInfo.parameters.custom.filter = "%" + oEvent.getParameter("query") + "%";
			} else {
				oBindingInfo.parameters.custom.filter = undefined
			}
			this._oList.bindItems(oBindingInfo);
}

My CAP handler with the filter handling:

const { Object } = srv.entities
const SF_Srv = await cds.connect.to('SF')

srv.on('READ', Object, async req => {

            if (!req._queryOptions.filter) {
                // share request context with the external service 
                return SF_Srv.tx(req).run(req.query);
            } else {
                //if filter provided, build manually a select statement using a where condition
                let input = req._queryOptions.filter;
                const tx = SF_Srv.transaction(req);
                return await tx.run(
                    SELECT
                        .from(Object)
                        .where`firstName like ${input} or lastName like ${input}`)
            }
    })

As alternative you could also add the where condition directly to the query object:

const { Object } = srv.entities
const SF_Srv = await cds.connect.to('SF')

srv.on('READ', Object, async req => {

            if (req._query.filter) {
                //if filter provided, build manually a select statement using a where condition
                let { query } = req
                let input = req._queryOptions.filter

                if (!query.SELECT.where) query.SELECT["where"] = []

                query.SELECT.where.push(
                    { ref: ['firstName'] }, 'like', { val: input }, "or",
                    { ref: ['lastName'] }, 'like', { val: input }, "or",
                    { ref: ['object'] }, 'like', { val: input })
            }

            // share request context with the external service 
            return SF_Srv.tx(req).run(req.query)
    })