2024 Jul 19 7:08 AM - edited 2024 Jul 26 1:02 PM
This is a task in the July Developer Challenge - "Reverse APIs".
This task gets you to add another API endpoint to the plain "REST" service you have, but will be a little different in its definition, implementation, and how it's called.
HTTP-based API endpoints, whatever the protocol, can differ in various ways, such as:
HTTP is an application protocol and there are already well understood semantic meanings for each of the methods (also known as "verbs", where the URLs are the "nouns"). There are some protocols that abuse or ignore these semantics, particularly the set of heavyweight "Web Services" from a decade or so ago, known as "WS-Deathstar" (due to their weight and complexity), SOAP, and even today we have GraphQL that arguably abuses HTTP as a mere transport layer. Yes, "abuse" is a strong word, but, as my bio hints at, it's an opinion I hold, and is one of many 🙂
Anyway, in addition to those differences listed earlier, one must also consider the duumvirate (yes, I did just use that word) of:
Idempotency (from the Latin, as is, of course, duumvirate) essentially means "having the same effect". To quote the excellent MDN docs on Idempotency:
An HTTP method is idempotent if the intended effect on the server of making a single request is the same as the effect of making several identical requests.
Examples of HTTP methods that have this idempotency characteristic are: GET, HEAD, PUT and DELETE (yes!).
A side effect in this context means that in handling or otherwise fulfilling an HTTP request, state is changed on the server. HTTP methods that do not have side effects are known as safe. Examples of HTTP methods that do have side effects, i.e. that are not safe, are POST, PUT and DELETE. Examples of HTTP methods that are side effect free, i.e. safe, are GET, HEAD and OPTIONS.
Semantic meanings are important, as they form part of the "contract" of behaviour between clients and servers.
Why am I telling you all this? Because CDL, CAP's definition language that you use to define your overall CDS model, has a couple of keywords that are important in this context.
In Capire's section on Providing Services, there's a subsection on Actions & Functions that explain more. I recommend you go and read that section, then come back here.
Did you notice the explanation was in the context of OData? That's because it's where the specific definitions originate, but these definitions make sense even outside the context of OData, such as when serving via the "REST" protocol.
In summary, we can see that both actions and functions are for providing API endpoints that represent resources beyond the standard CRUD+Q requests, although:
In addition (but not relevant for this task), both functions and actions can be bound or unbound. The idea of "unbound" is what we generally think of in terms of simple API endpoints. The "bound" concept is from OData, and is about calling an action or function relative to a specific entity. In such cases there's an extra implicit "binding parameter" that the implementation receives in the request, as a connection to the instance of the entity to which the called function is bound.
The first task in the context of this current plain "REST" service was an unbound function (note the brackets, despite no parameters):
/rest/plain/theAnswer()
You will also not need to use brackets at the end of the URL.
Here are the specific requirements for this task.
In the plain service you have already, define an unbound action that expects a list of integers. The implementation of that unbound action must determine the highest of those numbers, and return it as a single integer.
Note that if you are running CAP Node.js version 8 then you will have to annotate your action definition with @open. See the Open Types section of the blog post Automatic validation in OData and REST calls with CAP for more information.
Now you're ready to submit your CANDIDATE service, with this new API endpoint, to the TESTER!
Note that the TESTER will be calling your action via HTTP POST, and supplying the list of integers as an array, in a JSON representation, in other words, like this:
POST /rest/plain/highestValue HTTP/1.1 Host: localhost:8000 Content-Type: application/json Content-Length: 19 [54, 203, -3, 0, 1]
The task identifier you need to supply in the payload of your submission is: plain-highestValue.
You'll have already done this sort of thing previously so just head back there for the more detailed instructions if you need them, or to the the section titled "The Tester service, and making a test request" in the main challenge blog post.
You'll need to submit a JSON payload like this:
{ "communityid": "<your-community-id>", "serviceurl": "<the-URL-of-your-service>", "task": "plain-highestValue" }
the value for the communityid property should be your ID on this SAP Community platform (e.g. mine is "qmacro")
the value for the serviceurl property should be the absolute URL (i.e. including the scheme), of your CANDIDATE service which contains the API endpoint (see ℹ️ A note on URLs and services).
That's it!
Remember that you can check on your progress, and the progress of your fellow participants - all requests are logged and 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.
Until the next task, have fun, and if you have any questions or comments, leave them below!
2024 Jul 19 9:16 AM
That was great! I now know how to handle arrays, especially when passing through cURL. The ES6 spread syntax was a nice find using Google.
2024 Jul 19 3:11 PM
2024 Jul 19 3:46 PM
2024 Jul 19 5:32 PM
2024 Jul 21 8:18 AM - edited 2024 Jul 21 8:18 AM
Hehe.
BTW I was hoping someone would use `reduce` ... it's one of my fav functions 🙂
2024 Jul 19 8:01 PM
2024 Jul 19 11:33 PM
2024 Jul 21 8:19 AM
2024 Jul 19 11:55 PM
What a task this was!! Took me some debugging to get the sort working, I enjoyed every bit of it.
Here's my submission -
Thank you.
2024 Jul 21 8:19 AM
2024 Jul 20 10:51 AM - edited 2024 Jul 20 11:14 AM
2024 Jul 21 8:19 AM
2024 Jul 20 6:37 PM - edited 2024 Jul 20 7:58 PM
Would it not be more idiomatic for CAP to use a curl request with some input like below:
{ "input" = [54,203,-3,0,1] }
this would allow the service to type check "input" is an array of numbers with service definition like
action maxValue(input: array of Int32) returns Int32
the current service definition - at least my implementation will throw a HTTP 500 error if the request payload has an array element which is not a number
example: below request payload may throw an error with highestValue service
[54,203,-3,0,1,"i am not a number"]
2024 Jul 21 8:58 AM
Great thoughts! I'm not sure what is more idiomatic here, a simple array (`[1,2,3]`) or an array as the value of a property in an object (`{"vals": [1,2,3]}`). Can you share the exact error message(s) that are thrown when you supply a string in the array (like you showed)? And good work for digging in and finding out!
2024 Jul 21 3:19 PM - edited 2024 Jul 21 3:21 PM
Error with(`[1,2,3,"not a number"]`)
this maybe because I used Math.max() function
HTTP/1.1 500 Internal Server Error
{
"error": {
"code": "500",
"message": "Failed to validate return value of type 'cds.Int32' for custom action 'plain.highestValue': Value NaN is invalid."
}
}
error with (`{"input": [1,2,3,4,5,"not a number"]}`)
HTTP/1.1 400 Bad Request
{
"error": {
"message": "Value not a number is not a valid Int32",
"target": "input[5]",
"code": "400",
"@Common.numericSeverity": 4
}
}
2024 Jul 21 2:10 PM
2024 Jul 21 8:59 PM
Could you please tell me how to define an action that takes a plain array as input?
I can define an array with a name like this:
action highestValue(input: array of Int32) returns Int32;
But I cannot define it without a name like this:
action highestValue(array of Int32) returns Int32;
2024 Jul 23 1:08 PM
Hey there @MioYasutake - this is a very good question; just wanted you to know that I'm not ignoring it, but answering it may take me some time as I have some background explanation I want to share about it 😉
2024 Jul 24 9:02 PM - edited 2024 Jul 24 9:03 PM
Hi there - I wrote this up - let me know what you think! "Automatic validation in OData and REST calls with CAP" https://qmacro.org/blog/posts/2024/07/24/automatic-validation-in-odata-and-rest-calls-with-cap/
2024 Jul 22 2:15 AM
2024 Jul 22 10:41 AM
Took some time troubleshooting but Passed 🙂
Thanks for the fun challenges!
2024 Jul 22 12:32 PM
2024 Jul 22 6:09 PM
Took so many wrong turns trying to get to this:
Was tempted to use reduce but google sent me to Math.max... Really challenging. Thank you.
2024 Jul 23 6:21 AM
Excellent! Would it help to have a quick discussion on different ways in JS to do this? There are many, I could suggest a few ...
2024 Jul 24 9:44 AM
BTW, talking of Math.max, I wrote a short blog post on different ways to achieve what's required here in the implementation. You (and others, perhaps) may find it interesting: Highest value in JS - different ways.
2024 Jul 23 6:41 AM
2024 Jul 23 3:52 PM
2024 Jul 23 4:12 PM
What do the logs reveal? (cf logs [--recent] <your-cf-app-name>)
2024 Jul 24 9:29 PM
My submission for task 6. Finally, I got it working.
I was using CAP version 8, and that's why passing a plain array did not work (it got rejected). Thanks @qmacro, for the explanations in your blog post.
I noticed that if I didn't define any parameter for the action, data passed at runtime was still ignored. However, if I defined a dummy parameter as shown below, the data was passed to the event handler.
@open action highestValue(dummy: Int32) returns Int32;
2024 Jul 25 5:43 AM
Great stuff, thanks @MioYasutake. And good insight into the no vs dummy parameter aspect!
2024 Jul 27 1:49 AM
Here is my submission for the task-6.
2024 Jul 29 10:57 AM
Hello,
Done Task 6,
Thanks,
Manoj Kumar Potharaju.