Skip to Content
Technical Articles
Author's profile photo Alessandro Biagi

Extend SAP SuccessFactors on SAP BTP with CAP – Add Business Logic

Prerequisites

To follow-up with this blog post you must have read and completed the following previous blog posts in this series:

Create Code File

The business logic of the application is implemented via custom service handlers for the various operations executed on its entities (create, read, update, delete, etc.). Those handlers are defined in a module within a JavasScript file with the same name of the service but with the .js extension.

So, let’s create it!

1. On the left-hand pane of SAP Business Application Studio, select the srv folder, then click on the three dots to the right of the project name and select New File

Figure%201%20-%20Create%20New%20File

Figure 1 – Create New File

2. On the dialog name the file projman-service.js and click OK

Figure 2 – Set File Name

Service Module Coding

Copy and paste the code snippet below into the recently created file:

const cds = require('@sap/cds');

module.exports = cds.service.impl(async function () {
    /*** SERVICE ENTITIES ***/
    const {
        Project,
        Member,
        SFSF_User,
    } = this.entities;

    /*** HANDLERS REGISTRATION ***/
    // ON events

    // BEFORE events

    // AFTER events
});

Here we import the @sap/cds dependency and reference it as cds. Then, we implement the service module and, inside it, we reference three entities: Project, Member and SFSF_User, as we are supposed to develop handlers for them.

Finally, we make some comments as placeholders to mark where we will further put our code.

Organize Your Code

It is a best practice to have your code organized into files representing the nature of the code (i.e. utility functions should go into some “utils” file, handlers should go into some “handlers” file and so on). Those files represent your “code library”, so it’s appropriate to store them into some “lib” folder.

So, let’s create the lib folder and its contents.

1. In the Terminal press CTRL+C to terminate the cds watch command

Figure%203%20-%20Terminate%20cds%20watch

Figure 3 – Terminate cds watch

2. Type cd srv and press Enter

Figure%204%20-%20Change%20to%20srv%20directory

Figure 4 – Change to srv directory

3. Type mkdir lib and press Enter

Figure%205%20-%20Create%20lib%20directory

Figure 5 – Create lib directory

4. Type touch lib/handlers.js and press Enter

Figure 6 – Create handlers.js file

Handlers Coding

Now, let’s develop the required service handlers according to the business rules that have been defined in the series introduction and preparation.

SFSF_User Read Handler

1. On the left-hand pane expand the lib folder, then click on the hanlers.js file to open it

Figure 7 – Open handlers.js file

2. Copy and paste the following code snippet into handlers.js:

const cds = require('@sap/cds');

let userService = null;
let assService = null;

(async function () {
    // Connect to external SFSF OData services
    userService = await cds.connect.to('PLTUserManagement');
    assService = await cds.connect.to('ECEmployeeProfile');
})();

/*** HELPERS ***/

// Remove the specified columns from the ORDER BY clause of a SELECT statement
function removeColumnsFromOrderBy(query, columnNames) {
    if (query.SELECT && query.SELECT.orderBy) {
        columnNames.forEach(columnName => {
            // Look for column in query and its respective index
            const element = query.SELECT.orderBy.find(column => column.ref[0] === columnName);
            const idx = query.SELECT.orderBy.indexOf(element);

            if (idx > -1) {
                // Remove column from oder by list
                query.SELECT.orderBy.splice(idx, 1);
                if (!query.SELECT.orderBy.length) {
                    // If list ends up empty, remove it from query
                    delete query.SELECT.orderBy;
                }
            }
        });
    }

    return query;
}

/*** HANDLERS ***/

// Read SFSF users
async function readSFSF_User(req) {
    try {
        // Columns that are not sortable must be removed from "order by"
        req.query = removeColumnsFromOrderBy(req.query, ['defaultFullName']);

        // Handover to the SF OData Service to fecth the requested data
        const tx = userService.tx(req);
        return await tx.run(req.query);
    } catch (err) {
        req.error(err.code, err.message);
    }
}

module.exports = {
    readSFSF_User
}

3. Back to projman-service.js add the following lines right under const cds = require(‘@sap/cds’);

const {
    readSFSF_User
} = require('./lib/handlers');

4. Add the following line right under the comment // ON events

this.on('READ', SFSF_User, readSFSF_User);

Your projman-service.js code should now look like this:

Figure%209%20-%20Current%20stage%20of%20projman-service.js

Figure 8 – Current stage of projman-service.js

Let’s quickly analyze what’s been done.

In the lib/handlers.js file we connected cds to the SAP SuccessFactors external services, coded one helper function that removes undesired columns from the “order by” clause of a query’s select statement and, finally, coded the handler function to read the users from SAP SuccessFactors using the PLTUserManagement service.

The code logic is well explained in the detailed comments.

Now, let’s test that handler.

5. In the Terminal type cd .. and press Enter to go back to the project root directory

Figure%2010%20-%20Change%20back%20to%20project%20root

Figure 9 – Change back to project root

NOTE: newer versions of CDS have handed over to SAP Cloud SDK the creation of HTTP clients for making HTTP requests to external services, using the @sap-cloud-sdk/http-client node package. So, if you jump-started your CAP project, before such CDS update, that dependency might not have been included in your package.json, and, thus, not installed when you ran npm install. Before moving to the next step, please verify that you have @sap-cloud-sdk/http-client listed in the dependencies of your package.json and, if not, run: npm install @sap-cloud-sdk/http-client.

6. Once again type cds watch and press Enter. Then CTRL+Click on the http://localhost:4004 link to launch the application home page

Figure%2011%20-%20Application%20home%20page

Figure 10 – Application home page

7. Click on the SFSF_User link

Figure%2012%20-%20Users%20from%20SuccessFactors

Figure 11 – Users from SAP SuccessFactors

Now, you should be able to view the users that are being read from SAP SuccessFactors via the User entity from the PLTUserManagement service.

Other Handlers

That was the most important handler we should first implement as it’s the one responsible for bringing the SAP SuccessFactors’ employees into our application.

Now, we can “fast forward” and implement all the other handlers of our application at once.

1. Open the lib/hanlers.js file, then copy and paste the following code over the current content:

const cds = require('@sap/cds');
const namespace = 'sfsf.projman.model.db.';

let userService = null;
let assService = null;

(async function () {
    // Connect to external SFSF OData services
    userService = await cds.connect.to('PLTUserManagement');
    assService = await cds.connect.to('ECEmployeeProfile');
})();

/*** HELPERS ***/

// Remove the specified columns from the ORDER BY clause of a SELECT statement
function removeColumnsFromOrderBy(query, columnNames) {
    if (query.SELECT && query.SELECT.orderBy) {
        columnNames.forEach(columnName => {
            // Look for column in query and its respective index
            const element = query.SELECT.orderBy.find(column => column.ref[0] === columnName);
            const idx = query.SELECT.orderBy.indexOf(element);

            if (idx > -1) {
                // Remove column from oder by list
                query.SELECT.orderBy.splice(idx, 1);
                if (!query.SELECT.orderBy.length) {
                    // If list ends up empty, remove it from query
                    delete query.SELECT.orderBy;
                }
            }
        });
    }

    return query;
}

// Helper for employee create execution
async function executeCreateEmployee(req, userId) {
    const employee = await cds.tx(req).run(SELECT.one.from(namespace + 'Employee').columns(['userId']).where({ userId: { '=': userId } }));
    if (!employee) {
        const sfsfUser = await userService.tx(req).run(SELECT.one.from('User').columns(['userId', 'username', 'defaultFullName', 'email', 'division', 'department', 'title']).where({ userId: { '=': userId } }));
        if (sfsfUser) {
            await cds.tx(req).run(INSERT.into(namespace + 'Employee').entries(sfsfUser));
        }
    }
}

// Helper for employee update execution
async function executeUpdateEmployee(req, entity, entityID, userId) {
    // Need to check whether column has changed
    const column = 'member_userId';
    const query = SELECT.one.from(namespace + entity).columns([column]).where({ ID: { '=': entityID } });
    const item = await cds.tx(req).run(query);
    if (item && item[column] != userId) {
        // Member has changed, then:
        // Make sure there's an Employee entity for the new assignment
        await executeCreateEmployee(req, userId);

        // Create new assignment
        await createAssignment(req, entity, entityID, userId);
    }
    return req;
}

// Helper for assignment creation 
async function createAssignment(req, entity, entityID, userId) {
    const columns =  m => { m.member_userId`as userId`, m.parent(p => { p.name`as name`, p.description`as description`, p.startDate`as startDate`, p.endDate`as endDate` }), m.role(r => { r.name`as role` }) };
    const item = await cds.tx(req).run(SELECT.one.from(namespace + entity).columns(columns).where({ ID: { '=': entityID } }));
    if (item) {
        const assignment = {
            userId: userId,
            project: item.parent.name,
            description: item.role.role + " of " + item.parent.description,
            startDate: item.parent.startDate,
            endDate: item.parent.endDate
        };
        console.log(assignment);
        const element = await assService.tx(req).run(INSERT.into('Background_SpecialAssign').entries(assignment));
        if (element) {
            await cds.tx(req).run(UPDATE.entity(namespace + entity).with({ hasAssignment: true }).where({ ID: entityID }));
        }
    }
    return req;
}

// Helper for cascade deletion
async function deepDelete(tx, ID, childEntity) {
    return await tx.run(DELETE.from(namespace + childEntity).where({ parent_ID: { '=': ID } }));
}

/*** HANDLERS ***/

// Read SFSF users
async function readSFSF_User(req) {
    try {
        // Columns that are not sortable must be removed from "order by"
        req.query = removeColumnsFromOrderBy(req.query, ['defaultFullName']);

        // Handover to the SF OData Service to fecth the requested data
        const tx = userService.tx(req);
        return await tx.run(req.query);
    } catch (err) {
        req.error(err.code, err.message);
    }
}

// Before create/update: member
async function createEmployee(req) {
    try {
        // Add SFSF User to Employees entity if it does not exist yet
        const item = req.data;
        const userId = (item.member_userId) ? item.member_userId : null;
        if (userId) {
            await executeCreateEmployee(req, userId);
        }
        return req;
    } catch (err) {
        req.error(err.code, err.message);
    }
}

// After create: member
async function createItem(data, req) {
    try {
        // Create assignment in SFSF
        console.log('After create.');
        await createAssignment(req, req.entity, data.ID, data.member_userId);
        return data;
    } catch (err) {
        req.error(err.code, err.message);
    }
}

// Before update: member
async function updateEmployee(req) {
    try {
        // Need to check if team member was updated
        if (req.data.member_userId) {
            const ID = (req.params[0]) ? ((req.params[0].ID) ? req.params[0].ID : req.params[0]) : req.data.ID;
            const userId = req.data.member_userId;
            await executeUpdateEmployee(req, req.entity, ID, userId);
        }
        return req;
    } catch (err) {
        req.error(err.code, err.message);
    }
}

// Before delete: project or member
async function deleteChildren(req) {
    try {
        // Cascade deletion
        if (req.entity.indexOf('Project') > -1) {
            await deepDelete(cds.tx(req), req.data.ID, 'Activity');
            await deepDelete(cds.tx(req), req.data.ID, 'Member');
        } else {
            const item = await cds.tx(req).run(SELECT.one.from(namespace + req.entity).columns(['parent_ID']).where({ ID: { '=': req.data.ID } }));
            if (item) {
                await deepDelete(cds.tx(req), item.parent_ID, 'Activity');
            }
        }
        return req;
    } catch (err) {
        req.error(err.code, err.message);
    }
}

// After delete/update: member
async function deleteUnassignedEmployees(data, req) {
    try {
        // Build clean-up filter
        const members = SELECT.distinct.from(namespace + 'Member').columns(['member_userId as userId']);
        const unassigned = SELECT.distinct.from(namespace + 'Employee').columns(['userId']).where({ userId: { 'NOT IN': members } });

        // Get the unassigned employees for deletion
        let deleted = await cds.tx(req).run(unassigned);

        // Make sure result is an array
        deleted = (deleted.length === undefined) ? [deleted] : deleted;

        // Clean-up Employees
        for (var i = 0; i < deleted.length; i++) {
            const clean_up = DELETE.from(namespace + 'Employee').where({ userId: { '=': deleted[i].userId } });
            await cds.tx(req).run(clean_up);
        }
        return data;
    } catch (err) {
        req.error(err.code, err.message);
    }
}

// Before "save" project (exclusive for Fiori Draft support)
async function beforeSaveProject(req) {
    try {
        if (req.data.team) {
            // Capture IDs and users from saved members
            let users = []
            req.data.team.forEach(member => { users.push({ ID: member.ID, member_userId: member.member_userId }); });

            // Get current members
            let members = await cds.tx(req).run(SELECT.from(namespace + 'Member').columns(['ID', 'member_userId']).where({ parent_ID: { '=': req.data.ID } }));
            if (members) {
                // Make sure result is an array
                members = (members.length === undefined) ? [members] : members;

                // Process deleted members
                const deleted = [];
                members.forEach(member => {
                    const element = users.find(user => user.ID === member.ID);
                    if (!element) deleted.push(member);
                });
                for (var i = 0; i < deleted.length; i++) {
                    // Delete members' activities
                    await cds.tx(req).run(DELETE.from(namespace + 'Activity').where({ assignedTo_ID: { '=': deleted[i].ID } }));
                    if (req.data.activities) {
                        let idx = 0;
                        do {
                            idx = req.data.activities.findIndex(activity => activity.assignedTo_ID === deleted[i].ID);
                            if (idx > -1) {
                                req.data.activities.splice(idx, 1);
                            }
                        } while (idx > -1)
                    }
                }

                // Process added members
                const added = [];
                users.forEach(user => {
                    const element = members.find(member => user.ID === member.ID);
                    if (!element) added.push(user);
                });
                for (var i = 0; i < added.length; i++) {
                    await executeCreateEmployee(req, added[i].member_userId);
                }

                // Process updated members
                const updated = [];
                users.forEach(user => {
                    const element = members.find(member => user.ID === member.ID);
                    if (element) updated.push(user);
                });
                for (var i = 0; i < updated.length; i++) {
                    await executeUpdateEmployee(req, 'Member', updated[i].ID, updated[i].member_userId);
                }
            }
        }
        return req;
    } catch (err) {
        req.error(err.code, err.message);
    }
}

// After "save" project (exclusive for Fiori Draft support)
async function afterSaveProject(data, req) {
    try {
        if (data.team) {
            // Look for members with unassigned elementId
            let unassigned = await cds.tx(req).run(SELECT.from(namespace + 'Member').columns(['ID', 'member_userId']).where({ parent_ID: { '=': data.ID }, and: { hasAssignment: { '=': false } } }));
            if (unassigned) {
                // Make sure result is an array
                unassigned = (unassigned.length === undefined) ? [unassigned] : unassigned;

                // Create SFSF assignment
                for (var i = 0; i < unassigned.length; i++) {
                    await createAssignment(req, 'Member', unassigned[i].ID, unassigned[i].member_userId);
                }
            }
        }
        await deleteUnassignedEmployees(data, req);

        return data;
    } catch (err) {
        req.error(err.code, err.message);
    }
}

module.exports = {
    readSFSF_User,
    createEmployee,
    createItem,
    updateEmployee,
    deleteChildren,
    deleteUnassignedEmployees,
    beforeSaveProject,
    afterSaveProject
}

We just added three additional helpers: two for employee creation/update and one for the special assignment creation in SAP SuccessFactors.

Then, we added the required handlers for the before and after events.

The code logic is well explained in the comments details.

2. Open the srv/projman-service.js file, then copy and paste the following code over the current content:

const cds = require('@sap/cds');
const {
    readSFSF_User,
    createEmployee,
    updateEmployee,
    createItem,
    deleteChildren,
    deleteUnassignedEmployees,
    beforeSaveProject,
    afterSaveProject
} = require('./lib/handlers');

module.exports = cds.service.impl(async function () {
    /*** SERVICE ENTITIES ***/
    const {
        Project,
        Member,
        SFSF_User,
    } = this.entities;

    /*** HANDLERS REGISTRATION ***/
    // ON events
    this.on('READ', SFSF_User, readSFSF_User);

    // BEFORE events
    this.before('CREATE', Member, createEmployee);
    this.before('UPDATE', Member, updateEmployee);
    this.before('DELETE', Project, deleteChildren);
    this.before('DELETE', Member, deleteChildren);
    this.before('SAVE', Project, beforeSaveProject); // Fiori Draft support

    // AFTER events
    this.after('CREATE', Member, createItem);
    this.after('UPDATE', Member, deleteUnassignedEmployees);
    this.after('DELETE', Project, deleteUnassignedEmployees);
    this.after('DELETE', Member, deleteUnassignedEmployees);
    this.after('SAVE', Project, afterSaveProject); // Fiori Draft support
});

Here we just attached the handler functions to their corresponding events.

And,with that, we completed the coding of the business logic for our application.

Conclusion

Congratulations! You have successfully created the NodeJS code files and coded the complete business logic of the application! The next step is to add some CDS annotations to be used by the front-end UI – an SAP Fiori Elements HTML5 application.

NOTE: all the instructions provided in this blog post may apply to any CAP project that extends other applications (SAP S/4HANA Cloud, SAP Ariba, SAP CX, and other third-party), if you would like to try it in the future. You just need to adjust the business logic according to the business rules of your solution.

Please, do not hesitate to submit your questions in SAP Community through the Q&A tag link: https://answers.sap.com/index.html

Next blog post in this series

Assigned Tags

      11 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Mike Fields
      Mike Fields

      Good Moring Alessandro,

      This is an excellent series - but I must be missing an important step as I am unable to get the connection to work when clicking on SFSF_User

      I get the following:

      <error xmlns="http://docs.oasis-open.org/odata/ns/metadata">
        <code>500</code>
        <message>undefined</message>
        <target>Error during request to remote service: Cannot find module '@sap-cloud-sdk/http-client'          Require stack:
           - /home/user/projects/sfsf-projman/node_modules/@sap/cds/libx/_runtime/remote/utils/client.js
           - /home/user/projects/sfsf-projman/node_modules/@sap/cds/libx/_runtime/remote/Service.js
           - /home/user/projects/sfsf-projman/node_modules/@sap/cds/lib/index.js
           - /home/user/projects/sfsf-projman/node_modules/@sap/cds/bin/cds.js
           - /home/user/.node_modules_global/lib/node_modules/@sap/cds-dk/bin/cds.js
           - /home/user/.node_modules_global/lib/node_modules/@sap/cds-dk/bin/watch.js</target>
           <annotation term="Common.numericSeverity" type="Edm.Decimal">4</annotation>
      </error>
      As I am very new to BTP - I am not sure where to look to troubleshoot this and any suggestions on where to start would be greatly appreciated.
      I can see al the above listed files in the directory of my project - but must be missing something for the application to know where to look.
      Best Regards,
      Mike
      Author's profile photo Alessandro Biagi
      Alessandro Biagi
      Blog Post Author

      Hi Mike Fields,

      Please, see the update note added before step 6 of the "Handlers Coding" topic.

      Regards,

      Alessandro

      Author's profile photo Mike Fields
      Mike Fields

      Thank you so much!!

      That was the piece I was missing.

      Have a wonderful day!

      Author's profile photo Wenshang Zhang
      Wenshang Zhang

      Hi Alessandro

       

      This is a very informative and helpful series to kick start SF extension with BTP

      However I am having difficulties figuring out an error at step 7, when I click SFSF_User, I get below error :

      <error xmlns="http://docs.oasis-open.org/odata/ns/metadata">
      <code>500</code>
      <message>undefined</message>
      <target>Error during request to remote service: Request failed with status code 401</target>
      <annotation term="Common.numericSeverity" type="Edm.Decimal">4</annotation>
      </error>
      ---------
      I tried to re-do the entire tutorial but still having the same issue, it looks like I am missing some authentication settings but not sure where to look.
      Any suggestions will be much appreciated! Thank you in advance
      ---------
      the entire message in the terminal looks like this:
      [cds] - GET /projman/SFSF_User
      [remote] - Error: Error during request to remote service:
      Request failed with status code 401
      at run (/home/user/projects/sfsf-projman/node_modules/@sap/cds/libx/_runtime/remote/utils/client.js:297:31)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async RemoteService.<anonymous> (/home/user/projects/sfsf-projman/node_modules/@sap/cds/libx/_runtime/remote/Service.js:221:20)
      at async next (/home/user/projects/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:75:17)
      at async RemoteService.handle (/home/user/projects/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:73:10)
      at async RemoteService.handle (/home/user/projects/sfsf-projman/node_modules/@sap/cds/libx/_runtime/remote/Service.js:272:22)
      at async ApplicationService.readSFSF_User (/home/user/projects/sfsf-projman/srv/lib/handlers.js:46:16)
      at async next (/home/user/projects/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:75:17)
      at async ApplicationService.handle (/home/user/projects/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:73:10)
      at async _readCollection (/home/user/projects/sfsf-projman/node_modules/@sap/cds/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js:264:19) {
      statusCode: 502,
      reason: [AxiosError: Error during request to remote service:
      Request failed with status code 401] {
      code: 'ERR_BAD_REQUEST',
      config: {
      timeout: 60000,
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      proxy: false,
      method: 'get',
      baseURL: 'https://api10.successfactors.com/',
      url: '/odata/v2/User/User?$top=1000&$orderby=userId%20asc&$select=userId,username,defaultFullName,email,division,department,title',
      data: undefined
      },
      request: {
      method: 'GET',
      url: 'https://api10.successfactors.com//odata/v2/User/User?$top=1000&$orderby=userId%20asc&$select=userId,username,defaultFullName,email,division,department,title',
      headers: {
      Accept: 'application/json,text/plain',
      authorization: 'Basic ...',
      'accept-language': 'en',
      'x-correlation-id': '8b5524d9-ab37-4a91-a769-0e56d3c08a0d',
      'User-Agent': 'axios/0.27.2'
      }
      },
      response: {
      status: 401,
      statusText: 'Unauthorized',
      headers: {
      date: 'Tue, 20 Sep 2022 07:04:38 GMT',
      'content-type': 'text/plain;charset=utf-8',
      'transfer-encoding': 'chunked',
      connection: 'close',
      optr_cxt: '01000100007da801d7-38b2-11ed-b8a5-85f2588d91ff00000000-0000-0000-0000-000000000001-1 HTTP ;',
      'x-unique-id': '8b5524d9-ab37-4a91-a769-0e56d3c08a0d',
      'x-event-id': 'EVENT-UNKNOWN-UNKNOWN-ob01areboh15s-20220920170438-7263592',
      correlationid: '4b08cfae-21c0-4686-ba75-b2f073340186',
      requestno: '[6820030]',
      'sap-server': 'true',
      'error-code': 'LGN0020',
      server: 'BizX',
      'referrer-policy': 'strict-origin-when-cross-origin',
      'x-xss-protection': '1, mode=block',
      'x-content-type-options': 'nosniff',
      'strict-transport-security': 'max-age=16070400; includeSubDomains'
      },
      body: '[LGN0020]Incorrect format for HTTP Basic Authentication. Please encode your credentials (<username>@<companyID>:<password>) using Base64 format. For more information, visit https://help.sap.com/viewer/d599f15995d348a1b45ba5603e2aba9b/latest/en-US/5c8bca0af1654b05a83193b2922dcee2.html.'
      },
      correlationId: '8b5524d9-ab37-4a91-a769-0e56d3c08a0d'
      }
      }
      [cds] - Error: undefined
      at ApplicationService.readSFSF_User (/home/user/projects/sfsf-projman/srv/lib/handlers.js:48:13)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async next (/home/user/projects/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:75:17)
      at async ApplicationService.handle (/home/user/projects/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:73:10)
      at async _readCollection (/home/user/projects/sfsf-projman/node_modules/@sap/cds/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js:264:19)
      at async /home/user/projects/sfsf-projman/node_modules/@sap/cds/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js:470:16 {
      target: 'Error during request to remote service: \n' +
      'Request failed with status code 401',
      numericSeverity: 4,
      id: '1213576',
      level: 'ERROR',
      timestamp: 1663657478342
      }

       

      Author's profile photo Alessandro Biagi
      Alessandro Biagi
      Blog Post Author

      HI Wenshang,

      The issue is in your destination. HTTP 401 means the authentication to the API endpoint has failed (because of either an incorrect user name or password). And this is clearly stated in the complete error message:

      '[LGN0020]Incorrect format for HTTP Basic Authentication. Please encode your credentials (<username>@<companyID>:<password>) using Base64 format. For more information, visit https://help.sap.com/viewer/d599f15995d348a1b45ba5603e2aba9b/latest/en-US/5c8bca0af1654b05a83193b2922dcee2.html.

      If you go back to the first post in the series: Series introduction and preparation, in step 4 of he Initial Preparation, the subitem "b" clearly explains such formatting for SFSF authentication.

      Regards,

      Alessandro

       

      Author's profile photo Sandeep Surendranath
      Sandeep Surendranath

      Hello Alessandro,

       

      Thanks for sharing through this wonderful blog ... I am getting the below error when i click on /projman/SFSF_User and i am not able to see user data through..

       

      [cds] - GET /projman/SFSF_User
      [remote] - Error: Error during request to remote service:
      Failed to load destination.
      at run (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/libx/_runtime/remote/utils/client.js:299:31)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async RemoteService.<anonymous> (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/libx/_runtime/remote/Service.js:221:20)
      at async next (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:75:17)
      at async RemoteService.handle (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:73:10)
      at async RemoteService.handle (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/libx/_runtime/remote/Service.js:272:22)
      at async ApplicationService.readSFSF_User (/home/user/projects/ss/f01/sfsf-projman/srv/lib/handlers.js:46:16)
      at async next (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:75:17)
      at async ApplicationService.handle (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:73:10)
      at async _readCollection (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js:264:19) {
      statusCode: 502,
      reason: {
      message: 'Error during request to remote service: \n' +
      "Failed to load destination. Caused by: No user token (JWT) has been provided. This is strictly necessary for 'OAuth2SAMLBearerAssertion'.",
      request: {
      method: 'GET',
      url: 'https://apisalesdemo2.successfactors.eu/odata/v2/User?$top=1000&$orderby=userId%20asc&$select=userId,username,defaultFullName,email,division,department,title',
      headers: {
      accept: 'application/json,text/plain',
      'accept-language': 'en-US,en;q=0.9',
      'x-correlation-id': 'b8a0aae5-7524-4ede-9a2b-25ee4cf4d326'
      }
      },
      correlationId: 'b8a0aae5-7524-4ede-9a2b-25ee4cf4d326'
      }
      }
      [cds] - Error: undefined
      at ApplicationService.readSFSF_User (/home/user/projects/ss/f01/sfsf-projman/srv/lib/handlers.js:48:13)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async next (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:75:17)
      at async ApplicationService.handle (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/lib/srv/srv-dispatch.js:73:10)
      at async _readCollection (/home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js:264:19)
      at async /home/user/projects/ss/f01/sfsf-projman/node_modules/@sap/cds/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js:471:16 {
      target: 'Error during request to remote service: \nFailed to load destination.',
      numericSeverity: 4,
      id: '1575012',
      level: 'ERROR',
      timestamp: 1669984165445
      }

       

      Would you please point me in the right direction wanted to get the successfactors api working through the destination. I am using OAuth2SAMLBearerAssertion' which is configured as a destination in my subaccount.

       

      Thanks in advance,

       

      Sandeep S

      Author's profile photo Alessandro Biagi
      Alessandro Biagi
      Blog Post Author

      The message is quite clear: No user token (JWT) has been provided. This is strictly necessary for 'OAuth2SAMLBearerAssertion which means there's a configuration issue in your destination (either on the BTP or SFSF side).
      Two options: review and fix the configuration for the OAuth2SAMLBearerAssertion authentication or just go with the Basic Authentication which is quite suitable for the learning purpose of this series (as explained here).

      BR

      Author's profile photo Sandeep Surendranath
      Sandeep Surendranath

      The destination seems to be correct as the same destination is being used across other projects on BTP, Yet let me review the same once more. Meanwhile BASIC authentication is not recommended internally so cannot move ahead with that

      Author's profile photo Alessandro Biagi
      Alessandro Biagi
      Blog Post Author

      Did you read the note on step 4 (create destination) of the initial preparation in the series introduction? It says: "... for the learning purpose of this blog posts series we are using basic authentication in the destination, but be aware that this authentication method is deprecated for productive usage...". Isn't it clear that I'm not recommending basic authentication for productive scenarios? That is just for learning purposes which is the objective of the blog series! So, if you want to move forward with such objective and don't get stuck with this issue you can stick with basic authentication and, when the series is completed, get back to OAuht2 to investigate it.

      Author's profile photo Sandeep Surendranath
      Sandeep Surendranath

      Yes Doing the same at them moment and will keep you posted.

      BR

      Author's profile photo Alessandro Biagi
      Alessandro Biagi
      Blog Post Author

      On a side note: if your OAuth2 destination is set to use principal propagation then a BTP user must be authenticated through the OAuth flow to get a JWT which is forwarded to the CAP service and, from there, to the destination, so it can map the BTP user to an SFSF user (as per the definition of principal propagation). Therefore, it will require you to run the authentication flow locally in BAS before testing the service, which is something that's not covered by this blog series. In that case, you can either change the destination setup (or create a new one) to use a technical user instead of principal propagation or look into other blog posts (like this one) to learn how to setup the BAS project to run the authentication flow locally in development.