Inbound API
- Jacques Marais
Description
Helium provides inbound API functionality for DSL applications. Using this, a function can be annotated as a REST API function which can then be invoked from an external client. Functions can be annotated with @POST
, @DELETE
, @PUT
or @GET
. In addition, and if the API function returns a custom object or custom object collection, the @ResponseExclude
and @ResponseExpand
annotations can be used to specify which attributes should be excluded from the returned values and which relationships should be expanded in the returned values.
Supported API Function Return Types
Object | Object Collection | json | jsonarray | void | other primitives | |
---|---|---|---|---|---|---|
@POST | Yes | Yes | Yes | Yes | Yes | No |
@PUT | Yes | Yes | Yes | Yes | Yes | No |
@GET | Yes | Yes | Yes | Yes | No | No |
@DELETE | Yes | Yes | Yes | Yes | Yes | No |
In summary to the above:
- Custom object, custom object collections,
json
,jsonarray
andvoid
are valid return types for all API functions with the exception of GET functions, which do not supportvoid
. - GET functions cannot have a return type of
void
. - Primitive values, other than
json
andjsonarray
are not supported as valid return types for any API function.
Supported API Function Parameter Types
Object | Object Collection | json | jsonarray | Path Parameters | No Paramaters | Query Parameters | |
---|---|---|---|---|---|---|---|
@POST | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
@PUT | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
@GET | No | No | No | No | Yes | Yes | Yes |
@DELETE | No | No | No | No | Yes | Yes | Yes |
In summary to the above:
- All API functions can have path and query parameters.
- PUT and POST functions support custom object, custom object collection,
json
andjsonarray
body parameters. Only one of these parameters is allowed per API function. In other words only one parameter representing the payload body is allowed. - GET and DELETE API functions do not support any parameter representing a body. Only path and query parameters are supported in this case.
- Parameters are not compulsory for any API function.
API Response Codes
Default Response Codes
HTTP response codes are issued by the server in response to the request made. The codes can generally be used to determine whether the request was successful or not. Helium implements its own default responses for inbound API requests. There are described below.
- 400 Bad request: Could be result of incorrect arguments being sent when invoking the API.
- 200 Success: Applicable to all API function types. No error has occurred and API function executed successfully.
- 204 Success with no content: No error has occurred and API function executed successfully. Applicable to function with void return type.
- 404 Not found: The called URL could not be routed to an API function. A null value was returned from a GET API function.
Custom Response Codes
In addition to the above, Helium also allows developers to override the default response codes to an extent. This can be achieved by using the api:setStatusCode
built-in function and allows developers to use application logic to determine what response codes should be returned.
Note that if Helium encounters an exception before reaching the actual execution of the inbound API function the default error codes will apply. For example if the posted arguments cannot be parsed by Helium, it will result in a 400 response despite any logic in the inbound API function. Similarly if a API call is made that cannot be routed to an inbound API function helium will generate a 404 response.
api:setStatusCode
is therefore mostly relevant for overriding response codes for what Helium considers success responses by default.
See the Example 6 for a detailed example.
Format for API Function Body Arguments
Native JSON Body Arguments
The JSON body being posted should correspond to the function body parameter. If the function body parameter is of type json
, a JSON object should be posted. If the function body parameter is of type jsonarray
, a JSON array should be posted.
Custom Object and Custom Object Collection Body Arguments
The JSON body being posted should correspond to the function body parameter. If the parameter is a collection, a JSON array should be posted. If the parameter is an object, a JSON object should be posted. The JSON fields names should correspond to the DSL object attribute names. The values specified in the JSON data being posted should correspond to the DSL object attributes as follows:
string
attribute values are to be specified as strings, contained inside quotesint
attribute values are to be specified as numbers without any decimal partsenum
attribute values are to be specified as if they are strings, using only the enum value not including the enum type- For persistent objects, an
_id
field has to be specified. The value for the field should be a string representing a valid UUID. The value will be used as the unique identifier for the persistent object instance. - For non-persistent objects an _id field does not need to be specified but is allowed. If a value for
_id
is specified it will be used as the unique identifier for the non-persistent object instance. date
anddatetime
attribute values are to be represented by a numeric value representing the number of milliseconds since the unix epoch.blob
values are to be posted as base64 encoded string of file content byte arrays.- In addition to blob content as specified above, meta data should also be included for blob attributes. The name for these fields should be the name of the blob attributes appended with a specific key representing the type of meta data. Consider a case where our blob attributes name is "data". Values for the following fields should then also be posted:
data_fname__
is a sting field that represents the file name for the blobdata_size__
is an integer field that represents the file size for the blobdata_mtype__
is a sting field that represents the mime type for the blob
- Values for relationships can be posted using a string representation of the unique identifier of the related object and using a field name that corresponds to the relationship name on the DSL object. Posting values for many to many relationships is not supported.
Path Parameters
Path parameters can be specified as part of the API path by surrounding the parameter name with curly brackets when defining the API path inside the relevant API annotation. The part of the API path referencing a path parameter should contain only the single path parameter surrounded by curly brackets and no additional literal values or parameters. In addition to referencing a path parameter in the API path itself, path parameters should also be represented by a matching function parameter. Path parameters and their associated function parameters can be represented by enum types and primitive types with the exception of blob
, json
and jsonarray
. A recommended convention is to also describe the path parameter as part of the API path.
The following example demonstrates the points mentioned above:
@GET("v1/farmer/farmerId/{farmerId}") Farmer getFarmerById(uuid farmerId) { ... }
curl -v \ -u "user:pass" \ -X GET https://dev.mezzanineware.com/rest/mezzanine-extended-inbound-api-test/v1/farmer/farmerId/0d15a498-6a40-4d7a-a895-e3dde03598cc | python -m json.tool
Values for path parameters are converted similarly to how fields for JSON body arguments are converted. See the above section for reference. If an argument value sent for a path parameter cannot be converted to the type of the matching function parameter a 400 error code will be generated with an appropriate error message.
Query Parameters
Any function parameter specified for an API function, that does not represent a path parameter, can be used as a query parameter. Similarly to path parameters, query parameters can be represented by enum types and primitive types with the exception of blob
, json
and jsonarray
. All query parameters are treated as optional by Helium. This means that Helium will not validate that values for any query parameter are specified when an API is invoked. If a value for query parameter is not specified, it's value in the API function will be null.
The following example demonstrates the points mentioned above:
@GET("v1/purchase/latest") Purchase[] getLatestPurchases(int limit, bool showReturns) { ... }
curl -v \ -u "user:pass" \ -X GET "https://dev.mezzanineware.com/rest/mezzanine-extended-inbound-api-test/v1/purchase/latest?limit=20&showReturns=false" | python -m json.tool
Note in the above example how the start of the query string is denoted by '?' and how sections in the query string, each representing a different query parameter and argument value, are separated by '&'.
Values for query parameters are converted similarly to how path parameters and fields for JSON body arguments are converted. See the above sections for reference. If an argument value sent for a query parameter cannot be converted to the type of the matching function parameter a 400 error code will be returned with an appropriate error message. If a parameter is referenced in the query string and that parameter was not defined as a query parameter in the API function, a 400 error code with appropriate error message will also be returned by the API.
@ResponseExclude and @ResponseInclude
When returning a custom object or custom object collection from an API function the default behaviour for the response values will be as follows:
- All fields that represent attributes are included by default.
- For both persistent and non-persistent objects and collections the unique identifier for the object, as used by Helium, is included as an
_id
field. - For relationships that represent many to one and one to one multiplicities, the unique identifier of the related object is included with the relationship name as the field.
- Relationships that represent one to many are excluded from the results by default.
Consider the following example showing the default behaviour:
persistent object Person { string name; string surname; string mobileNumber; @OneToMany Pet pets via owner; } persistent object Pet { string name; string age; PET_TYPE type; } enum PET_TYPE { Dog, Molerat }
@GET("v1/person/mobileNumber/{mobileNumber}") Person getPerson(string mobileNumber) { ... }
The above model and API function results in the following result when invoking the API:
{ "_id": "00e7b7e1-8506-4043-b4f3-fb29220540d4", "mobileNumber": "27761231234", "name": "Jack", "surname": "Marques" }
The default behaviour as shown above can be overridden by making use of the @ResponseExclude
and @ResponseExpand
annotations in order to exclude an attribute or relationships for the return values or to expand a relationship in the return value. The paths specified for both @ResponseExclude
and @ResponseExpand
must represent valid attribute and relationship paths but the paths can be chained to, for example, expand on a relationship and to then exclude a field for the object represented in the expanded relationship:
@ResponseExclude("_id") @ResponseExclude("mobileNumber") @ResponseExpand("pets") @ResponseExclude("pets.age") @GET("v1/person/mobileNumber/{mobileNumber}") Person getPerson(string mobileNumber) { ... }
The modified API function above results in the following return value:
{ "name": "Jack", "pets": [ { "_id": "ac33a973-6c86-479f-8236-7c71b52b0c2c", "name": "Jasmine", "owner": "d90b5f26-693d-40d3-abeb-fb028a6bbdee", "type": "Dog" }, { "_id": "c81b4856-35d3-4e15-8ee4-ca1ec500af81", "name": "Markus", "owner": "d90b5f26-693d-40d3-abeb-fb028a6bbdee", "type": "Dog" } ], "surname": "Marques" }
More examples of this can be seen here.
API URL
The URL for Helium APIs contains the following sections:
Section | Example | Description |
---|---|---|
Base URL | https://dev.mezzanineware.com/rest | The base URL for inbound API REST functionality. This will differ accordingly depending on which server is being used. In this case, its
https://myapp.heliumapps.com/rest/ |
API friendly app name | mezzanine-extended-inbound-api-test | The second part represents to API friendly name. The value is needs to be set an app when creating the app on the Helium Core web application or by updating the app on the Helium Core web application. |
Resource | v1/purchase/latest | The final part of the URL refers to the value specified when annotating an function as an inbound REST API post function. Note that we add |
Gotchas, Conventions and Best Practices
- Version your REST API resources as described in this document for example
v1/myresouce.
When defining REST resource paths follow proper conventions to indicate the inherent relationships between entities being interacted with. For example
v1/farmer/profile/documentation.
Don't use verbs such as get, post, delete or put in your API paths. The intent of a specific API should be implied by the type of API, such as GET, PUT, DELETE, POST and the entities and relationships implied from the API path.
Post functionality as implemented in the Helium DSL is strictly that of object creation when posting persistent object data. This means that if two API calls are made to post persistent data for the same object type and using the same value for
_id
, the API call will fail due to a duplicate key violation.- Put functionality as implemented in the Helium DSL is strictly that of update when putting persistent object data. If an object instance is to be created, post should be used and if an object instance is to be updated, put is to be used.
- For POST or PUT API functions with persistent object or collections as a body parameter, the value for
_id,
of each object instance, must be specified when invoking the API. - For POST or PUT API functions with non-persistent object or collections as a body parameter, the value for
_id,
of each object instance, can be specified when invoking the API but is not required.
Examples
Example 1
This example highlights the following:
- Usage of the
@POST
annotation. - How a persistent object or persistent object collection can be posted.
- The use of a non-persistent object as an API function return type.
object ApiResponse { datetime requestReceived; datetime requestProcessed; string message; bool success; }
persistent object StockUpdate { string stockName; int level; decimal price; date stocktakeDate; @ManyToOne Shop shop via stockUpdates; @ManyToOne Stock stock via stockUpdate; }
@POST("v1/stock/stockupdate") ApiResponse postStockUpdate(StockUpdate stockUpdate) { ... }
@POST("v1/stock/stockupdates") ApiResponse postStockUpdates(StockUpdate[] stockUpdates) { ... }
curl -u 'user:pwd' \ -H "Content-Type: application/json" \ -X POST "https://dev.mezzanineware.com/rest/mezzanine-tut-lesson-25/v1/stock/stockupdate" \ -d '{ "_id":"fb94f312-2f99-4d40-889a-414d4b09f1ac", "level":200, "price":100, "stocktakeDate":1522326277000, "shop":"4575470d-ee0a-474d-bd5f-1c370c6fc817", "stock":"80dc5655-9600-440b-86c0-614ccaef11fe" }'
curl -u 'user:pwd' \ -H "Content-Type: application/json" \ -X POST "https://dev.mezzanineware.com/rest/mezzanine-tut-lesson-25/v1/stock/stockupdates" \ -d '[ { "_id":"b54fb253-7f61-4a5a-9ae8-fcf42c495892", "level":200, "price":100, "stocktakeDate":1522326277000, "shop":"4575470d-ee0a-474d-bd5f-1c370c6fc817", "stock":"80dc5655-9600-440b-86c0-614ccaef11fe" }, { "_id":"ae9b5e85-9c7f-4062-9cbd-84060dc2267d", "level":500, "price":160, "stocktakeDate":1522326277000, "shop":"4575470d-ee0a-474d-bd5f-1c370c6fc817", "stock":"d5f7cb6d-69ef-4e01-953d-151d89792155" } ]'
Example 2
This example highlights the following:
- Using the
@POST
annotation - Posting of a non-persistent object.
- Posting a blob value.
- Making use of a path parameter.
object ApiFarmerDocumentation { uuid governmentAssistanceCertificateId; blob governmentAssistanceCertificate; }
@POST("v1/farmer/mobileNumber/{mobileNumber}/profile/documentation") ApiResponse postFarmerDocumentation(ApiFarmerDocumentation farmerDocumentation, string mobileNumber) { ... }
curl -u 'user:pass' \ -H "Content-Type: application/json" \ -X POST "https://dev.mezzanineware.com/rest/v1/farmer/mobileNumber/27761231234/profile/documentation" \ -d '{ "farmerMobileNumber":"27763303624", "governmentAssistanceCertificateId":"7c3c66dd-992d-489a-a4e0-99da1f225422", "governmentAssistanceCertificate":"WW91IGFyZSBhcHByb3ZlZCBmb3IgZ292ZXJubWVudCBhc3Npc3RhbmNlLg==", "governmentAssistanceCertificate_fname__":"FarmerGovernmentAssistance.txt", "governmentAssistanceCertificate_size__":43, "governmentAssistanceCertificate_mtype__":"text/plain" }'
Example 3
This example highlights the following:
- Using the
@GET
annotation. - Using
json
as an API return type.
@GET("v1/admin/count") json countRecords() { json result = "{}"; result.jsonPut("workouts", countWorkouts()); result.jsonPut("workoutEntries", countWorkoutEntries()); result.jsonPut("workoutExerciseGroups", countWorkoutExerciseGroups()); result.jsonPut("workoutDataUpload", countUploadedWorkoutData()); return result; }
curl -v \ -u "user:pass" \ -X GET https://dev.mezzanineware.com/rest/mezzanine-extended-inbound-api-test/v1/admin/count
{ "workoutDataUpload": 1, "workoutEntries": 2817, "workoutExerciseGroups": 719, "workouts": 191 }
Example 4
This example highlights the following:
- Using the
@GET
annotation. - Using the
@ResponsExclude
annotation. - Using the
@ResponseExpand
annotation.
@NotTracked persistent object Workout { . . . json entity; } persistent object WorkoutEntry { . . . @ManyToOne Workout workout via workoutEntries; @ManyToOne WorkoutExerciseGroup workoutExerciseGroup via workoutEntries; } persistent object WorkoutExerciseGroup { . . . @ManyToOne Workout workout via workoutExerciseGroups; }
@ResponseExclude("_id") @ResponseExclude("entity") @ResponseExpand("workoutEntries") @ResponseExclude("workoutEntries._id") @ResponseExpand("workoutExerciseGroups") @ResponseExclude("workoutExerciseGroups._id") @ResponseExpand("workoutExerciseGroups.workoutEntries") @ResponseExclude("workoutExerciseGroups.workoutEntries._id") @GET("v1/workout/latest") Workout getLatestWorkoutDetails() { return getLatestWorkoutSummary(); }
curl -v \ -u "user:pass" \ -X GET https://dev.mezzanineware.com/rest/mezzanine-extended-inbound-api-test/v1/workout/latest
Example 5
This example highlights the following:
- Using the
@DELETE
annotation. - Using a query parameter.
- Using a date path parameter.
void
return type for an API function.
@DELETE("v1/workout/date/{workoutDate}") void deleteWorkoutByDate(bool purge, date workoutDate) { Workout workout = getWorkoutForDate(workoutDate); if(workout != null) { if(purge == true) { Workout:delete(workout); } else { workout.deleted = true; } } }
curl -v \ -u "user:pass" \ -X DELETE https://dev.mezzanineware.com/rest/mezzanine-extended-inbound-api-test/v1/workout/date/1507464000000?purge=true
Example 6
This example highlights the following:
- Using the @POST annotation
- Native json return type
- Setting the API response status code using
api:setStatusCode
// Create a json response and set the status code json createResponse(int code, string message) { api:setStatusCode(code); json responseBody = "{}"; responseBody.jsonPut("code", code); responseBody.jsonPut("message", message); return responseBody; } @POST("v1/support/ticket") json postSupportTicket(SupportTicket supportTicket) { // Check the database for this ticket SupportTicket existingTicket = SupportTicket:read(supportTicket._id); // Validate duplicate id if(existingTicket != null) { return createResponse(400, "A support ticket with the specified id is already present in the system"); } // Validate the request content if(supportTicket.text == null || String:length(supportTicket.text) == 0) { return createResponse(400, "A text value describing the support request has to be specified"); } // Populate all fields for the support ticket supportTicket.receivedTime = Mez:now(); supportTicket.spam = false; supportTicket.resolved = false; supportTicket.deleted = false; supportTicket.save(); // Return a success response return createResponse(200, "The support ticket was successfully posted"); }
curl -v -u 'user:pwd' -H "Content-Type: application/json" -X POST "https://dev.mezzanineware.com/rest/mezzanine-status-code-test/v1/support/ticket" -d '{ "_id":"0e83d835-963e-4c9c-8340-75269d7c6c57", "text":"", "senderNumber":"27761231234" }'
< HTTP/1.1 400 Bad Request < Date: Fri, 30 Oct 2020 11:48:10 GMT < Content-Type: application/json < Content-Length: 88 < Connection: keep-alive < Server: Helium 1.19.1-SNAPSHOT < X-Powered-By: Mezzanine < { [88 bytes data] 100 185 100 88 100 97 437 482 --:--:-- --:--:-- --:--:-- 920 * Connection #0 to host jm.stb.mezzanineware.com left intact * Closing connection 0 { "code": 400, "message": "A text value describing the support request has to be specified" }
curl -v -u 'usr:pwd' -H "Content-Type: application/json" -X POST "https://dev.mezzanineware.com/rest/mezzanine-status-code-test/v1/support/ticket" -d '{ "_id":"0e83d835-963e-4c9c-8340-75269d7c6c57", "text":"Please help..", "senderNumber":"27761231234" }'
< HTTP/1.1 200 OK < Date: Fri, 30 Oct 2020 11:48:17 GMT < Content-Type: application/json < Content-Length: 67 < Connection: keep-alive < Server: Helium 1.19.1-SNAPSHOT < X-Powered-By: Mezzanine < { [67 bytes data] 100 166 100 67 100 99 1063 1571 --:--:-- --:--:-- --:--:-- 2634 * Connection #0 to host jm.stb.mezzanineware.com left intact * Closing connection 0 { "code": 200, "message": "The support ticket was successfully posted" }
Built-in APIs
In addition to the APIs developed as part of a DSL application, some internal APIs are also available through the same mechanism described in the tutorial. These include APIs to query app health and return the current list of scheduled functions for an app. Below are examples demonstrating these APIs.
Example: Scheduled Functions
As with all app inbound APIs, requests can be made using the friendly API name or the app ID:
curl -v -u 'usr:pwd' \ -H "Content-Type: application/json" \ -X GET "https://dev.mezzanineware.com/rest/mezzanine-meta-test/built-in/meta/scheduled-function"
curl -v -u 'usr:pwd' \ -H "Content-Type: application/json" \ -X GET "https://dev.mezzanineware.com/rest/0f9172b2-cd0e-4e5b-9b81-7e61fb601edb/built-in/meta/scheduled-function"
[ { "schedule": "*/5 * * * *", "unit": "LogUnit", "function": "logSomething" }, { "schedule": "*/2 * * * *", "unit": "LogUnit", "function": "logSomethingElse" } ]
Example: App Health
The example request shown below makes use of the app ID, but once again, either the app ID or the friendly API name may be used.
curl -v -u 'usr:pwd' \ -H "Content-Type: application/json" \ -X GET "https://dev.mezzanineware.com/rest/b2482dd5-866e-410e-9a52-5d9217987521/built-in/meta/health"
Apps that have an active current release and that can be reached through inbound API will result in a 200 response code with a body as shown below:
{ "appId": "b2482dd5-866e-410e-9a52-5d9217987521", "appName": "Test App 1 - Has release and compiles", "jiraCode": "jira-code", "appDataSource": "default", "appDeployedTstamp": 1692968922892, "appDeployedTstampDesc": "2023-08-25 01:08:42", "appReleaseId": "d216247e-82e2-4f62-bf46-8df0b662f79f", "appReleaseName": "My Test Release", "appReleaseUser": "admin", "appReleaseTstamp": 1692968920937, "appReleaseTstampDesc": "2023-08-25 01:08:40", "compilable": true, "error": null, "errorDetail": [], "disabled": false, "disabledMessage": "", "disabledReason": "None", "locked": false, "lockedMessage": "" }
Apps that do not have an active current release will result in a 500 response as shown below:
500 The ID for the application instance is not associated with a compilable application instance
Apps that have an active release that causes compile or schema migration issues will result in a 500 response code and respective bodies as shown below:
{ "appId": "192cb078-acdf-6219-11b2-6582636ab4bb", "appName": "Test App 3 - Has release but does not compile-SQL", "jiraCode": null, "appDataSource": "default", "appDeployedTstamp": 1695201850258, "appDeployedTstampDesc": "2023-09-20 09:24:10", "appReleaseId": "09f4b36f-75cf-4c10-a4e2-35a10fd65b65", "appReleaseName": "My Test Release - Uncompilable", "appReleaseUser": "admin", "appReleaseTstamp": 1695201837993, "appReleaseTstampDesc": "2023-09-20 09:23:57", "compilable": false, "error": "com.mezzanine.persistence.AppPersistenceException: SQL error in /sql-scripts/create_default_stock.sql: ERROR: relation \"somethingthatdoesntexist\" does not exist\n Position: 15", "errorDetail": [], "disabled": false, "disabledMessage": "", "disabledReason": "None", "locked": false, "lockedMessage": "" }
{ "appId": "ae19aeca-65ab-6d5b-ed09-8a748292b6b6", "appName": "Test App 4 - Has release but does not compile-DSL", "jiraCode": null, "appDataSource": "default", "appDeployedTstamp": 1695209063722, "appDeployedTstampDesc": "2023-09-20 11:24:23", "appReleaseId": "ff274989-061a-4673-b5a5-631af35fd63e", "appReleaseName": "My Test Release - Uncompilable 2", "appReleaseUser": "admin", "appReleaseTstamp": 1695204028206, "appReleaseTstampDesc": "2023-09-20 10:00:28", "compilable": false, "error": "com.mezzanine.program.web.compiler.CompilerException", "errorDetail": [ "FakeInboundMessages:5 - The variable is defined to be a collection of type InvalidType, but this custom type is not defined in the application", "FakeInboundMessages:18 - The variable is defined to be a collection of type InvalidType, but this custom type is not defined in the application" ], "disabled": false, "disabledMessage": "", "disabledReason": "None", "locked": false, "lockedMessage": "" }
Additional Mentions and References
For more detail on the inbound API feature as described on this page, please see the related documentation below, specifically the Helium tutorial lesson which describes detailed examples and provides complete accompanying source code.