2024 Jul 05 6:57 AM - edited 2024 Jul 09 9:08 PM
This is a task in the July Developer Challenge - "Reverse APIs".
So, you've completed the warm-up task 0 and have managed to get a simple CAP server up, running and accessible from the cloud, and therefore (critically for this month's challenge) accessible by the TESTER component. Great! Now it's time to create your first API endpoint. What does that mean, and what do you have to do?
As this is your first task where you have to create both a service and an endpoint, some help and hints are given. So it's a little more reading for you, but don't worry, the subsequent task descriptions won't be as verbose.
An API endpoint very often exists in the wider context of a service, which you can think of as being a "container" for one or more endpoints. An OData service will typically offer multiple entity set resources, plus perhaps some action and function imports.
For example, the classic Northwind service offers various "collections" which normally translate into "entity sets", such as Customers, Products and Categories. And as for function and action imports, the TripPin service, for example, sports a function import called GetNearestAirport*.
*I was surprised to see that my nearest airport is apparently in Rome, but then remembered that there are only 15 airports in the dataset.
Not every service is OData, of course. But in the context of enterprise computing, it's an extremely well respected, understood and used open standard. And CAP makes it child's play to create an OData service and fill it with API endpoints.
And as the idea of this challenge is to use CAP, then the first thing you'll need to do is create a simple service. You can modify the existing CatalogService that you got for free when you initialised the CAP project in Task 0. You know, the one that exposes a Books entity set, and is defined declaratively, with CDL, in srv/cat-service.cds:
using my.bookshop as my from '../db/data-model'; service CatalogService { @readonly entity Books as projection on my.Books; }
But what we recommend is that you leave that CatalogService as it is, and create your own new service. And while you could define it in the same srv/cat-service.cds file, instead, we recommend you take advantage of the wonderfully flexible nature of the CDS compiler ... and create a (pair of) new file(s), in the srv/ directory, in which you will define your service.
Hint: As you'll see, the name of the service you'll be required to create here is basic, so, within the srv/ directory, why not use the filename basic.cds. Furthermore, the API endpoint you'll be required to create in this task is an unbound function within that service, so (as with all actions and functions) you'll need to provide an implementation, so why not use the filename basic.js for that and place that file next to (in the same directory as) basic.cds.
Here are the specific requirements for this task.
Define a new service.
Make sure the service name is basic.
Have it be served via the OData V4 protocol (this is what all services are served via by default with CAP). But don't have it served at the standard path for an OData V4 protocol based service (which would be /odata/v4/basic), instead, have it served at the simpler path /basic.
The service, at least at this point in the challenge, needs to have a single API endpoint, specifically one that is called ping, that can be called with the HTTP GET method, takes no parameters, and returns a JSON payload that looks like this:
{ "@odata.context": "$metadata#Edm.String", "value": "pong" }
It is critical (to test success) that what's returned in the value property here is the JSON string "pong".
In other words, this API endpoint should be defined as an unbound function.
When the service, containing this API endpoint, is fully defined, and served via the OData V4 protocol, the metadata document should look like this:
<?xml version="1.0" encoding="utf-8"?> <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"> <edmx:Reference Uri="https://sap.github.io/odata-vocabularies/vocabularies/Common.xml"> <edmx:Include Alias="Common" Namespace="com.sap.vocabularies.Common.v1"/> </edmx:Reference> <edmx:Reference Uri="https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.xml"> <edmx:Include Alias="Core" Namespace="Org.OData.Core.V1"/> </edmx:Reference> <edmx:DataServices> <Schema Namespace="basic" xmlns="http://docs.oasis-open.org/odata/ns/edm"> <EntityContainer Name="EntityContainer"> <FunctionImport Name="ping" Function="basic.ping"/> </EntityContainer> <Function Name="ping" IsBound="false" IsComposable="false"> <ReturnType Type="Edm.String"/> </Function> </Schema> </edmx:DataServices> </edmx:Edmx>
If you've not defined (here: in srv/basic.cds) and written an implementation (here: in srv/basic.js) for a function or action before, then you need to know a couple of things.
First, know that you need to implement such functions or actions via the on event. Also, know that there are two implementation styles in CAP Node.js*, described in the Capire section How to provide custom service implementations?:
Note that the older style is translated automatically, behind the scenes, to the ES6 class based style anyway.
* As mentioned in the Introduction to this challenge, hints are for the Node.js flavour of CAP, but of course you're welcome to use the Java flavour if you prefer.
Once you've got your service defined, and a simple implementation ready, with an on handler for the ping event, you're ready.
It is definitely worth testing it yourself first, e.g. with curl, or Postman, or whatever tool you prefer for making HTTP calls. With your server running (on, let's say, the default local CAP server port of 4004), make a request like this:
curl -s --url 'localhost:4004/basic/ping()'
and the reponse should look like this:
{"@odata.context":"$metadata#Edm.String","value":"pong"}
Now you're ready to submit your CANDIDATE service, with the specific API endpoint, to the TESTER!
The task identifier you need to supply in the payload of your submission is: basic-ping.
While some of you lovely folks with a more rebellious and hacker nature (I'm looking at you @ajmaradiaga and @gphadnis2000 :-)) have already kicked the tyres of the TESTER service, this should be the first time you'll be using the TESTER service.
Most of what you need to know is described in the the section titled "The Tester service, and making a test request" in the main challenge blog post, so head over to that section for a quick refresher first, then come back here.
Now, to have your freshly minted API endpoint in this task tested, you'll need to submit a JSON payload like this:
{ "communityid": "<your-community-id>", "serviceurl": "<the-URL-of-your-service>", "task": "basic-ping" }
Here's an example (don't use these values for communityid and serviceurl, they're specific to me and provided here just for illustration):
{ "communityid": "qmacro", "serviceurl": "https://c0df-85-255-235-188.ngrok-free.app/basic", "task": "basic-ping" }
Note that the value for the communityid property should be your ID on this SAP Community platform (e.g. mine is "qmacro").
Note also the value for the serviceurl property should be the absolute URL (i.e. including the scheme), of your CANDIDATE service (see ℹ️ A note on URLs and services), not the specific API endpoint.
Of course, you need to make sure your new service and API endpoint are available to the TESTER, so supplying http://localhost:4004/basic as the value for the serviceurl property is not going to work. Make sure you either redeploy your CAP project (if you're pushing to Cloud Foundry) or still have your ngrok tunnel up and running (if you're using the ngrok tunnel approach).
You'll need to submit that JSON payload in a POST request to this endpoint:
https://developer-challenge-2024-07.cfapps.eu10.hana.ondemand.com/tester/testServer
and you'll need to supply a Content-Type header stating that the representation of the payload resource has a media type of application/json.
Here's a curl invocation doing exactly that, by way of example (and using my specific values shown in the illustration above):
curl \ --data '{"communityid":"qmacro","serviceurl":"https://c0df-85-255-235-188.ngrok-free.app/basic","task":"basic-ping"}' \ --header 'Content-Type: application/json' \ --url 'https://developer-challenge-2024-07.cfapps.eu10.hana.ondemand.com/tester/testServer'
You'll see one of two types of responses- an error response, or a test result response.
If there's something wrong with your call then you'll get an error response. For example, if you haven't supplied a value for the communityid property, you'll get an HTTP 400 error response with a payload like this:
{ "error": { "code": "400", "message": "Missing Community ID value", "@Common.numericSeverity": 4 } }
Similarly, if the TESTER cannot reach your endpoint, you'll get HTTP 500 error response with a payload like this:
{ "error": { "code": "500", "message": "Error calling service endpoint", "@Common.numericSeverity": 4 } }
Other similar errors may occur, for example if you supply a missing or invalid task identifier or service URL.
If the TESTER manages to reach your API endpoint successfully, it will then check that the response returned is what is required and expected. In the case of this task, the response required and expected is the static value "pong". If that is what is received, a positive test result response will be returned, and will look like this:
{"@odata.context":"$metadata#Edm.String","value":"PASS"}
If a different value is received, then a negative test response will be returned, and will look like this:
{"@odata.context":"$metadata#Edm.String","value":"FAIL"}
That's it!
Each time there's a call to the TESTER by any participant, and a test result response can be issued, that test request is logged. So you can check on your progress, and the progress of your fellow participants.
The logged requests are available in an entity set served by the TESTER service. The entity set URL is https://developer-challenge-2024-07.cfapps.eu10.hana.ondemand.com/tester/Testlog and being an OData V4 entity set, all the normal OData system query options are available to you for digging into that information.
But that's all for now on that.
Until the next task, have fun, and if you have any questions or comments, leave them below!
2024 Jul 05 1:51 PM
2024 Jul 05 3:59 PM
2024 Jul 05 4:31 PM
I was successful with my call to the tester service, but I couldn't work out the OData filter for "result". It's an SByte, so I couldn't use $filter=result eq 'PASS'. Filtering by communityid worked ok.
2024 Jul 06 10:31 AM - edited 2024 Jul 06 10:32 AM
Hi there @mwn
What were you attempting to do? Get a list of test requests that succeeded, using the result property in a filter? How about: https://developer-challenge-2024-07.cfapps.eu10.hana.ondemand.com/tester/Testlog?$filter=result%20eq...
2024 Jul 06 1:27 PM
It looks like a typo on my part, as the query works ok now.
2024 Jul 05 11:01 PM
2024 Jul 06 12:40 PM
2024 Jul 06 9:52 PM
2024 Jul 07 1:55 PM
2024 Jul 07 2:28 PM
2024 Jul 07 4:26 PM
2024 Jul 08 5:47 AM - edited 2024 Jul 09 9:05 AM
Nice one @MatLakaemper ... Yes, you'll notice that on this first API endpoint, I'm being explicitly lenient on the service path name ... things will be stricter for tasks later in this challenge 😉
2024 Jul 07 9:06 PM
My submission for Task 1 :
The metadata of the basic endpoint
GET call the endpoint 'basic' with unbound function ping:
And then the testerServer call
2024 Jul 07 10:58 PM
2024 Jul 08 7:00 AM
Hello qmacro,
Here is my submission for Task 1.
Thanks,
Manoj Kumar Potharaju.
2024 Jul 08 11:00 AM
First time doing a developer challenge and having fun 🙂
Posting the entry
Testlog
2024 Jul 08 5:22 PM - edited 2024 Jul 08 5:29 PM
As one who doesn't work on a BTP site, the local development was relatively painless but the deployment needed a lot of work. Deleting a previous database (From the June Developer Challenge), installing @Sap/xssec and editing the cdsrc.json ("auth": "mocked"):
2024 Jul 08 6:07 PM - edited 2024 Jul 08 6:08 PM
Hi @geek - this is why I specifically included settings, and specifically included `{"auth":"mocked"}` in Task 0, which referred to the example simple deployment in the main blog post, detailed in the Simple Deployment to BTP section ... did that not work for you? There would have been no requirement to delete any database, nor install `@sap/xssec`, nor add the mocked setting to `.cdsrc.json`.
2024 Jul 08 9:35 PM
Hello @qmacro !
Here is my response from the Basic service:
And the subsequent call of the tester service:
2024 Jul 09 7:04 AM
2024 Jul 09 9:46 AM
Hi Gaurav, you've fallen foul of a little mechanism that is designed to discourage folks just using other folks' services to pass the tests 🙂 It looks like you've used two different SAP Community IDs:
i.e. `gaurav.phadnis` and `gphadnis`, and now you're using a third ID here: `gphadnis2000`. How many SAP Community IDs do you have? I suspect only one, `gphadnis2000`. So you have caused yourself a little bit of pain here 🙂
I would suggest, as it's still early on in the task sequence, redeploying your app with a different name other than `gphadnis-simplest-deployment`, and then also making sure you're consistently using the correct (and only) SAP Community ID value you have, in each of the calls to the TESTER, i.e. `gphadnis2000`.
Hope that helps!
2024 Jul 09 10:26 AM
2024 Jul 09 11:45 AM
2024 Jul 09 7:29 AM
Good morning @qmacro - the documentation above mentions a communityid. I'm actually a little confused by whether you mean the SAP Community User Id or the SAP Community User DisplayName? Your Community User ID appears to be 53 (see https://community.sap.com/t5/user/viewprofilepage/user-id/53 ) yet you are using your displayname of qmacro. Does it matter what we use?
2024 Jul 09 9:37 AM
Hey there Andrew - great to see you here on this Developer Challenge! Sorry for the confusion. I've added an extra paragraph to "The Tester service, and making a test request" section in the main blog post:
Note the "communityid" value is your name on this SAP Community platform, for example mine is "qmacro". My numeric SAP Community user ID is 53 but the SAP Community ID we're using here is exactly the same as the one we used for the Developer Challenge on APIs last year.
Basically, the terminology I use on this platform is:
There were a couple of places in the main body of the description for Task 0 where I incorrectly used the term "SAP Community name", but I've corrected those now.
HTH, and thanks for keeping me straight!
dj
2024 Jul 09 8:40 AM
2024 Jul 09 12:22 PM
2024 Jul 09 1:54 PM - edited 2024 Jul 09 1:54 PM
Nice shell environment! (plus using ngrok) 🚀
2024 Jul 09 7:40 PM
2024 Jul 10 4:25 PM - edited 2024 Jul 10 4:49 PM
I have a little html page which can be used to provide a quick summary of the successful calls. Because of CORS issues I couldn't easily get the test results, so I launch then in a new tab and then copy/paste them into a text block.
<script>
function analyze() {
var v = document.getElementById("buffer").value;
var results = JSON.parse(v);
var tasks = [];
var ids = [];
for (i=0;i<results['value'].length;i++) {
task = results['value'][i].task;
id = results['value'][i].communityid;
if (!tasks.includes(task)) tasks.push(task);
if (!ids.includes(id)) ids.push(id);
};
tasks.sort();
document.write("<h2>Results of challenge</h2>");
document.write('<Table border="1"><tr><th>communityid</th>');
for (i=0;i<tasks.length;i++) {
document.write("<th>"+tasks[i]+"</th>");
};
document.write("</tr>");
for (i=0;i<ids.length;i++) {
id = ids[i];
document.write("<tr><td>"+ids[i]+"</td>");
for (j=0;j<tasks.length;j++) {
task = tasks[j];
found = " ";
for (x=0;x<results['value'].length;x++) {
if (results['value'][x].communityid == id && results['value'][x].task == task) found = "X";
};
document.write('<td style="text-align:center">'+found+"</td>");
};
document.write("</tr>");
};
document.write("</table>");
};
</script>
Click the link <a href="https://developer-challenge-2024-07.cfapps.eu10.hana.ondemand.com/tester/Testlog?$filter=result%20eq%20%27PASS%27&$orderby=communityid&$select=communityid,task" target="_new" >here</a> to launch a new tab with the results, and then copy paste the contents below.
Please ensure you have the source/raw/unformatted version.
<input id="buffer" size="200" oninput="analyze()"/>
Create your own .html file, paste the source code, and launch the page. You can also try it at https://michaelnicholls.github.io/results.html
2024 Jul 12 6:38 AM
2024 Jul 10 5:56 PM
2024 Jul 11 10:31 PM
2024 Jul 15 2:23 PM
2024 Jul 18 8:02 AM
2024 Jul 15 8:34 PM
2024 Jul 16 7:41 PM
how to remove the odata/v4 in the deployed url to the cloud foundary.
2024 Jul 17 4:30 PM
I'm guessing that's a question ... and if so, you might want to take a look at this part of Capire: https://cap.cloud.sap/docs/node.js/cds-serve#path. Good luck! 🚀
2024 Jul 23 5:25 AM