Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
Mujeeb
Participant
The concept of Side-by-Side Extension within the realm of SAP involves an approach that focuses on expanding or improving pre-existing SAP systems without making alterations to their fundamental core operations. This methodology permits the creation and implementation of supplementary customized applications or features that run in conjunction with the established SAP environment. The primary objective of this approach is to curtail the potential for disturbances to the system while introducing new functionalities.

In simpler terms, Side-by-Side Extension in SAP entails enhancing SAP systems without changing their essential workings. This allows for the development and deployment of extra customized applications or features alongside the current SAP setup, all the while reducing the risk of disrupting the system's normal operations.

Recently I was working on a requirement where we need to attach a document to purchase order as GOS object. I spent some time to figure out ways to achieve this. I came accross API which can be used achieve the objective. API_CV_ATTACHMENT_SRV this service enables you to manage attachments and attachment URLs for business objects. For this service to work we need to have Harmonized Document Management(HDM) configured so that Generic Object Services can work.











CAP offers built-in functionality for handling binary data and media content. If you have previously established an entity to house the media, you now have the option to avoid replicating the identical document to other systems such as DMS or GOS. Instead, you can directly share a hyperlink to the GOS service, allowing users to access the document seamlessly via your core system. This approach remains effective even when the document itself is stored in the HANA database service (or any other chosen database). This concept can be expanded further to encompass storing documents within DMS, utilizing document store services, or employing S3 buckets for attachment storage, all while obtaining hyperlinks for easy retrieval.

Scenario: 


Link media object to SAP standard document.

Pre-requisite: 



  1. Need to know how to create a basic CAP project.













Architecture



Development:



  1. Create CAP project with name Attachment. From terminal you can run below command
    cds init Attachment​


  2. Create data/data-model.cds file. And create entity to store the Media
    namespace Media.db;

    entity MediaFile {
    key id : Integer;
    @Core.MediaType : mediaType
    content : LargeBinary;
    @Core.IsMediaType : true
    mediaType : String;
    fileName : String;
    url : String;
    };​


  3. Create a service in srv->media-service.cds to expose the MediaFile Entity.
    using {Media.db as db} from '../db/data-model';


    service MediaService @(path : '/media') {
    entity MediaFile as projection on db.MediaFile;
    };​


  4. In srv->media-service.js add below logic on the hook handlers.
    const SequenceHelper = require("./library/SequenceHelper");
    const { Readable, PassThrough } = require("stream");
    const cds = require('@sap/cds');
    cds.env.features.fetch_csrf = true

    module.exports = cds.service.impl(async function (srv) {

    const {
    MediaFile
    } = this.entities;


    /**
    * Handler method called before creating data entry
    * for entity Mediafile.
    */

    srv.before('CREATE', MediaFile, async (req) => {
    const db = await cds.connect.to("db");
    // Create Constructor for SequenceHelper
    // Pass the sequence name and db
    const SeqReq = new SequenceHelper({
    db: db,
    sequence: "MEDIA_ID",
    table: "MediaFile",
    field: "ID"
    });
    //Call method getNextNumber() to fetch the next sequence number
    let seq_no = await SeqReq.getNextNumber();
    // Assign the sequence number to id element
    req.data.id = seq_no;
    //Assign the url by appending the id
    req.data.url = `/media/MediaFile(${req.data.id})/content`;


    });

    /**
    * Handler method called on reading data entry
    * for entity Mediafile.
    **/
    srv.on("READ", MediaFile, async (req, next) => {
    if (!req.data.id) {
    return next();
    }

    //Fetch the url from where the req is triggered
    const url = req._.req.path;
    //If the request url contains keyword "content"
    // then read the media content
    if (url.includes("content")) {
    const id = req.data.id;
    var tx = cds.transaction(req);
    // Fetch the media obj from database
    var mediaObj = await tx.run(
    SELECT.one.from("Media_db_MediaFile", ["content", "mediaType"]).where(
    "id =",
    id
    )
    );
    if (mediaObj.length <= 0) {
    req.reject(404, "Media not found for the ID");
    return;
    }
    var decodedMedia = "";
    decodedMedia = new Buffer.from(mediaObj.content, 'base64'
    );
    return _formatResult(decodedMedia, mediaObj.mediaType);
    } else return next();
    });
    });

    function _formatResult(decodedMedia, mediaType) {
    const readable = new Readable();
    readable.push(decodedMedia);
    readable.push(null);
    return {
    value: readable,
    '*@odata.mediaContentType': mediaType
    }
    }


  5. Create srv->library->SequenceHelper.js and add below logic
    module.exports = class SequenceHelper {
    constructor (options) {
    this.db = options.db;
    this.sequence = options.sequence;
    this.table = options.table;
    this.field = options.field || "ID";
    }

    getNextNumber() {
    return new Promise((resolve, reject) => {
    let nextNumber = 0;
    switch (this.db.kind) {
    case "hana":
    this.db.run(`SELECT "${this.sequence}".NEXTVAL FROM DUMMY`)
    .then(result => {
    nextNumber = result[0][`${this.sequence}.NEXTVAL`];
    resolve(nextNumber);
    })
    .catch(error => {
    reject(error);
    });

    break;
    case "sql":
    case "sqlite":
    default:
    reject(new Error(`Unsupported DB kind --> ${this.db.kind}`));
    }
    });
    }
    };​


  6. Inside root folder of your project run below command.
    cds add hana​


  7. Create the file definition for your HDB Sequence. Create a file called MEDIA_ID.hdbsequence inside db > src folder
    SEQUENCE "MEDIA_ID" START WITH 1 MAXVALUE 2999999999​

    add above lines to MEDIA_ID.hdbsequence file.

  8. Deploy the database artifacts
    cds deploy --to hana


  9. Install dependancy
    npm install​

     

  10. Login to BTP account and create the HANA HDI shared instance and test the application once by
    cds watch --profile hybrid​



  11. Create a test.http file in your root folder of project and send a request like belowvalidate the file The steps I've shared with you so far might be familiar from other sources, as they are commonly discussed in various blogs. Now, let's delve into the GOS aspect, which sets this process apart.

  12. Get the EDMX file of attachment API from API Hub and upload the EDMX file from API Hub to your cap project and then run the cds import. Make sure you upload the edmx file to root folder and then run below command.
    cds import API_CV_ATTACHMENT_SRV.edmx   ​


  13. Once import is done, you should see the API entry in packge.json file, you can add the destination details/Basic authentication details as shown below

  14. Modify your media-service.js file as belowhere you need to give correct Business object name in BusinessObjectTypeName and document number in LinkedSAPObjectKey (make sure that user has all the sufficient authorization to perform the operation in your ERP system) final code should look like this.
    const SequenceHelper = require("./library/SequenceHelper");
    const { Readable, PassThrough } = require("stream");
    const cds = require('@sap/cds');
    cds.env.features.fetch_csrf = true

    module.exports = cds.service.impl(async function (srv) {

    const {
    MediaFile
    } = this.entities;


    /**
    * Handler method called before creating data entry
    * for entity Mediafile.
    */
    const InvoiceAttachment = await cds.connect.to('API_CV_ATTACHMENT_SRV');

    srv.before('CREATE', MediaFile, async (req) => {
    const db = await cds.connect.to("db");
    // Create Constructor for SequenceHelper
    // Pass the sequence name and db
    const SeqReq = new SequenceHelper({
    db: db,
    sequence: "MEDIA_ID",
    table: "MediaFile",
    field: "ID"
    });
    //Call method getNextNumber() to fetch the next sequence number
    let seq_no = await SeqReq.getNextNumber();
    // Assign the sequence number to id element
    req.data.id = seq_no;
    //Assign the url by appending the id
    req.data.url = `/media/MediaFile(${req.data.id})/content`;

    var host = req.headers.host;
    const PostingResult = await InvoiceAttachment.send('CreateUrlAsAttachment',
    { 'MIMEType': 'text/html', 'UrlDescription': 'blog', 'Url': 'http://' + host + req.data.url,
    'BusinessObjectTypeName': 'BUS2012','LinkedSAPObjectKey': '4500000###', 'x-csrf-fetch':'fetch'});

    });

    /**
    * Handler method called on reading data entry
    * for entity Mediafile.
    **/
    srv.on("READ", MediaFile, async (req, next) => {
    if (!req.data.id) {
    return next();
    }

    //Fetch the url from where the req is triggered
    const url = req._.req.path;
    //If the request url contains keyword "content"
    // then read the media content
    if (url.includes("content")) {
    const id = req.data.id;
    var tx = cds.transaction(req);
    // Fetch the media obj from database
    var mediaObj = await tx.run(
    SELECT.one.from("Media_db_MediaFile", ["content", "mediaType"]).where(
    "id =",
    id
    )
    );
    if (mediaObj.length <= 0) {
    req.reject(404, "Media not found for the ID");
    return;
    }
    var decodedMedia = "";
    decodedMedia = new Buffer.from(mediaObj.content, 'base64' );
    return _formatResult(decodedMedia, mediaObj.mediaType);
    } else return next();
    });
    });

    function _formatResult(decodedMedia, mediaType) {
    const readable = new Readable();
    readable.push(decodedMedia);
    readable.push(null);
    return {
    value: readable,
    '*@odata.mediaContentType': mediaType
    }
    }


  15. Activate the API_CV_ATTACHMENT_SRV from iwfnd/maint_service tcode, Perform the step no 11 again and see the resultjust double click on the title you will see the file attachment.


In case you need, you can push the actual document itself to the S4 system, in that case you can go for the AttachmentContentSet entity, where you need to pass the data to API instead of url.

Conclusion:


By combining CAP's binary data management capabilities with the innovative integration of the Generic Object Services (GOS), you can elevate your system's efficiency and user experience to new heights. This approach not only optimizes media handling but also introduces a distinct edge that sets your implementation apart from the conventional methods described in many other blogs. Embrace the power of GOS to unlock a realm of possibilities and deliver a solution that stands out in its functionality and effectiveness. Your journey to seamless binary data management starts here, guided by the synergy of CAP and GOS.

 

Happy learning

Mujeeb

 

 
2 Comments
Labels in this area