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

[SuccessFactors] Check if IAS is activated for a tenant

Go to Upgrade Center, select Platform in the Filter By Dropdown. In the Optional Upgrades Column, search for:

Initiate the SAP Cloud Identity Services Identity Authentication Service Integration

If the entry exists, IAS is not yet set up. If it does not exist, the IAS configuration is probably already done.

[Cloud Identity Services] Assign SuccessFactors user to IAS group via transformation

Source System: SuccessFactors
Target System: Identity Authentication

When using ias.api.version 1

https://blogs.sap.com/2021/03/29/ias-integration-with-sap-successfactors-application-2-sync-users-using-identity-provisioning-serviceips/

            {
                "condition": "$.emails[0].value =~ /.*@abc.com.*/",
                "constant": "DEV_IDP1",
                "targetPath": "$.groups[0].value"
            },
            {
                "condition": "$.emails[0].value =~ /.*@def.com.*/",
                "constant": "DEV_AzureAD",
                "targetPath": "$.groups[1].value"
            },

When using ias.api.version 2

https://help.sap.com/docs/identity-provisioning/identity-provisioning/enabling-group-assignment

         {
            "condition":"($.emails EMPTY false)",
            "constant":[
               {
                  "id":"00f8ab94-a732-48fa-9169-e51f87b8dcd5"
               },
               {
                  "id":"01231139-4711-4a28-8f9d-6745843ef716"
               }
            ],
            "targetVariable":"assignGroup"
         }

[CAP] Dynamically set destination in package.json for an external connection

  "cds": {
    "requires": {
      "sfsf": {
        "kind": "odata-v2",
        "credentials": {
          "destination": "<set during runtime>",
          "path": "/odata/v2",
          "requestTimeout": 18000000
        }
      }
    }
  },
  /*
   * Handover query to some external SF OData Service to fecth the requested data
   */  
  srv.on("READ", Whatever, async req => {

        const sf_api_def = cds.env.requires['sfsf'] //defined in package.json

        sf_api_def.credentials.destination = "myDestinationName" //set your Destination name, could come from a customizing table

        const sfsfSrv = await cds.connect.to(sf_api_def)

        return await sfsfSrv.run(req.query)
    })

[BTP] Create sap-successfactors-extensibility service providing a technicalUser

Prerequisite, you have registered an SAP SuccessFactors system in your Global Account (see here). Creating the sap-successfactors-extensibility service can be done via command line:

#Created the service instance
#An HTTP destination on a subaccount level with the same name as the service instance name is automatically generated
cf create-service sap-successfactors-extensibility api-access myInstanceName -c '{"systemName": "SFCPART000000","technicalUser": "sfadmin"}'

#Bind the instance to an application
cf bind-service myApp-srv myInstanceName 

Find an explanation of the parameters here: https://help.sap.com/docs/btp/sap-business-technology-platform/authentication-type-json-file

This service instance will result in creating:

  • a separate OAuth2 client application on SFSF side (can find in SF in Manage OAuth2 Client Applications)
  • a separate destination definition on a BTP sub-account level

The technicalUser parameter can be specified only during creation. There is no possibility to provide it afterwards using cf update-service. It may be possible to manually update the technicalUser in the destination, which got automatically created. But I did not test this yet.

Of course, the same service creation can also be done via mta.yaml.

resources:
 #####################################################################################################################
  # SuccessFactors Extensibility Service
  #####################################################################################################################
  - name: myInstanceName 
    type: org.cloudfoundry.managed-service
    #type: org.cloudfoundry.existing-service
    parameters:
      service: sap-successfactors-extensibility
      service-plan: api-access
      config:
        systemName: SFCPART000000 # <-- Provide your system name
        technicalUser: sfadmin

For initial deployment, you need the line type: org.cloudfoundry.managed-service. For all further deployments, you have to comment that line out and comment in the next line type: org.cloudfoundry.existing-service. Else you will receive an error. Read more about that behavior here:https://github.com/SAP-samples/successfactors-extension-calculate-employee-seniority/issues/2

[CAP] Check if User exists in SuccessFactors

To check if a record exists, you can simply use the HEAD query.

“HEAD is almost identical to GET, but without the response body.” (https://www.w3schools.com/tags/ref_httpmethods.asp)

Unfortunately, there is no shortcut in CAP for a HEAD query, so simply use send with method HEAD.

    sfsfSrv = await cds.connect.to('sfsf')

    try {
        await sfsfSrv.send('HEAD', `/User('${userId}')`)
        return true  // status 200
    } catch (err) {
        return false // status 404
    }

[SuccessFactors] Create JavaScript Date-Object from DateTimeOffset

The SuccessFactors oData v2 API is returning timestamps in Unix Epoch format (unix-style milliseconds since 1/1/1970).

Many timestamp fields are of type Edm.Int64. When receiving the milliseconds as Integer, you can directly create a date-object of it using Date(1658237847).

But some timestamps are of type Edm.DateTimeOffset, i.e.: "createdDate": "/Date(1652252620000+0000)/".
When binding a timestamp property with an ODataModel, the internal lib datajs will convert the /Date(...)/ value to a standard JS date-object.

But in my case I manually had to convert the timestamp and this is the shortest way I found to convert the epoch string into a JS date-object.

// SF epoch date string
const SFdateString = '/Date(1652252620000+0000)/' 

// remove the '/' on both sides and create the date object
const oDate = eval('new ' + SFdateString .slice(1, -1))

console.log(typeof oDate )
console.log(oDate )

const oDateTimeFormat = sap.ui.core.format.DateFormat.getDateTimeInstance({
          pattern: "YYYY-MM-dd HH:mm"
})
return oDateTimeFormat.format(oDate)


[CAP] Query teamMembersSize from SuccessFactors

const sfsfSrv = await cds.connect.to('sfsf')

// Option 1: Query Notation
const response = await sfsfSrv.run(SELECT`teamMembersSize`.from`User`.where`userId = ${req.user.id}`)
console.log("option 1: " + response[0].teamMembersSize)

// Option 2: HTTP method-style
const response2 =  await sfsfSrv.get(`/User('${req.user.id}')/teamMembersSize/$value`)
console.log("option 2: " + response2)

[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)
    })

[ABAP] Parse SF Entity Key from URI

  METHOD get_entitykey_from_uri.

    " Pattern: Entity\((.*)\), 
    DATA(pattern) = iv_entity && `\((.*)\)`.

    TRY.
        DATA(matcher) = cl_abap_matcher=>create( pattern     = pattern
                                                 text        = iv_uri
                                                 ignore_case = abap_true ).
      CATCH cx_sy_invalid_regex.
    ENDTRY.

    TRY.
        DATA(lt_matches) = matcher->find_all( ).
        ASSIGN lt_matches[ 1 ] TO FIELD-SYMBOL(<s>).

        rv_entity_key = substring( val = iv_uri off = <s>-offset len = <s>-length ). 

      CATCH cx_sy_no_current_match.
    ENDTRY.

  ENDMETHOD.