Publication tutorial
Difficulty: Medium
Pre-requisiteβ
- An ewam 7 installed, see eWam Dev installation for example
- Take 5 min to read about eWam Web Framework introduction
- Take 5 min to read about eWam Web Publication concepts
Install the tutorial files:
npx @ewam/installer --tutorials
Install @ewam/wam-cloud
npm i @ewam/wam-cloud
npm run ewam:cli -- --import ./node_modules/@ewam/wam-cloud/src --use-exit-code
note
Before you start it is recommended that you know about the following concept:
- HTTP verbs (Get, post, ...)
- HTTP status codes (200,...)
- JSON
- Rest APIs
- Curl or any other http client
Objectivesβ
This tutorial will guide you to the different steps need to expose and document the 4 rest APIs.
Get /api/version
=> to retrieve your application versionPost /api/clients
=> to create a specific clientGet /api/clients
=> To retrieve all the clientsGet /api/clients/{name}
=> To retrieve a specific client
You also learn to secure it with an API key.
A note on API Design best practicesβ
Follow conventions
The explanation below is given by the excellent best practice document from Microsoft. Here we will try to follow this section.
The HTTP protocol defines a number of methods that assign semantic meaning to a request. The common HTTP methods used by most RESTful web APIs are:
- GET retrieves a representation of the resource at the specified URI. The body of the response message contains the details of the requested resource.
- POST creates a new resource at the specified URI. The body of the request message provides the details of the new resource. Note that POST can also be used to trigger operations that don't actually create resources.
- PUT either creates or replaces the resource at the specified URI. The body of the request message specifies the resource to be created or updated.
- PATCH performs a partial update of a resource. The request body specifies the set of changes to apply to the resource.
- DELETE removes the resource at the specified URI.
The effect of a specific request should depend on whether the resource is a collection or an individual item. The following table summarizes the common conventions adopted by most RESTful implementations using the e-commerce example. Not all of these requests might be implementedβit depends on the specific scenario.
Resource | POST | GET | PUT | DELETE |
---|---|---|---|---|
/customers | Create a new customer | Retrieve all customers | Bulk update of customers | Remove all customers |
/customers/1 | Error | Retrieve the details for customer 1 | Update the details of customer 1 if it exists | Remove customer 1 |
/customers/1/orders | Create a new order for customer 1 | Retrieve all orders for customer 1 | Bulk update of orders for customer 1 | Remove all orders for customer 1 |
The differences between POST, PUT, and PATCH can be confusing.
A POST request creates a resource. The server assigns a URI for the new resource, and returns that URI to the client. In the REST model, you frequently apply POST requests to collections. The new resource is added to the collection. A POST request can also be used to submit data for processing to an existing resource, without any new resource being created.
A PUT request creates a resource or updates an existing resource. The client specifies the URI for the resource. The request body contains a complete representation of the resource. If a resource with this URI already exists, it is replaced. Otherwise a new resource is created, if the server supports doing so. PUT requests are most frequently applied to resources that are individual items, such as a specific customer, rather than collections. A server might support updates but not creation via PUT. Whether to support creation via PUT depends on whether the client can meaningfully assign a URI to a resource before it exists. If not, then use POST to create resources and PUT or PATCH to update.
A PATCH request performs a partial update to an existing resource. The client specifies the URI for the resource. The request body specifies a set of changes to apply to the resource. This can be more efficient than using PUT, because the client only sends the changes, not the entire representation of the resource. Technically PATCH can also create a new resource (by specifying a set of updates to a "null" resource), if the server supports this.
PUT requests must be idempotent. If a client submits the same PUT request multiple times, the results should always be the same (the same resource will be modified with the same values). POST and PATCH requests are not guaranteed to be idempotent.
Implementation guidelinesβ
To expose web services with ewam 7, you need to:
- Create a service class
- Declare it in the configuration file
- Protect it via middlewares
- Add tests
Init your git repoβ
We will develop this tutorial with git, and in particular the git flow.
git init
git add -A
git commit -m "feat:project initialization"
- go into github and create a new private repository
- Set the name as
tuto-ws
git remote add origin https://github.com/{github-id}/tuto-ws.git
git push --set-upstream origin master
Now visit your github repository, all your code shoud be there.
Feature 1: /api/versionβ
Specificationβ
Let's create a very simple service that will respond to this request:
GET http://localhost:8080/stateless/api/version
with the following response : "1.0.0"
Create the feature branchβ
Let's create a feature branch:
git checkout -b feat/api-version
Add the codeβ
Create the file:
; aInsuranceWebService (aWebService) (Def Version:2) (Implem Version:2)
[ServiceDescriptor(path: '/api')]
class aInsuranceWebService (aWebService)
uses ServiceDescriptorAnnotation
;return the system version
function Version return CString
return '1.0.0'
endFunc
function InitService return Boolean
uses Net, aMethodDesc
Net.MapMethod('/version', Net.CreateHttpMethod('[GET]', self, MetaModelEntity(self.Version)))
endFunc
; This let you customize how parameter and response are being de/serialized
function GetServiceAdapterFactory return aDataAdapterFactory override
uses Doc
return Doc.wGetSystemAdapterFactory
endFunc
Pro tip
If you use VSCode, you may want to have syntax highlighting for .wam
file. Get ewam-vscode-extension.vsix
from Wyde Package Manager. In case you do note have access to Wyde Package Manager, ask ewam-vscode-extension.vsix
to site admin. Or build the vsix
yourself from repo https://github.com/MphasisWyde/ewam-vscode-extension. Note that eWam-vscode-extension is still WIP and not yet available on VS Code Market Place
Note
InitService
can have any parameters you want that can be next set from config-runtime.json
(see Service Configuration section). That's why this method need to be always fully created and not an override.
Or copy the source from the template:
move ./tutorials/rest-app/1 ./src
Import the code
npm run ewam:cli -- --import ./src
You should see the following output:
Loading:aClassDef[aInsuranceWebService]
Cannot find aClassDef[aInsuranceWebService]
Creating 1 new entities
Loading:aClassDef[aInsuranceWebService]
aClassDef[aInsuranceWebService] : created
aClassDef[aInsuranceWebService] : definition updated
aClassDef[aInsuranceWebService]: import completed
Process stopping
Process stopped
service ide closed
Service Configurationβ
Modify the file config-runtime.json
inside of your Admin folder to declare the service aInsuranceWebService
inside a 'stateless' session:
{
"runtimeMode": "Application",
"applications": {
"stateless": {
"type": "stateless",
"services": [
{
"className": "aInsuranceWebService"
},
{
//Swagger 3.0 documentation
"className": "aDocumentationService"
}
]
}
}
}
Pro tip
If you use VSCode, you may notice some auto-completion and validation on that file. This is done with a file you can find in your .vscode
folder. Check out how it is done: https://code.visualstudio.com/docs/languages/json#_json-schemas-and-settings
Note
All parameters defined in InitService
(see Add the Code section) can be set here after the corresponding className
.
Start and testβ
Close ewam and start your service:
npm start
Try it out: Open the url: http://localhost:8080/stateless/api/version
Push your branchβ
git add -A
git commit -m "[feat]: exposing /api/version "
git push
Create a PR and ask someone to review your code. This person will probably tell you that you need to add some test, so let's do that next before we merge this branch.
Test /api/versionβ
JS Testingβ
It is fairly easy to test this using javascript. Create the file test.js
require("./server.js")
var assert = require("assert")
var ewam = require("@ewam/node-hosting")
const { exit } = require("process")
const stateless = ewam.getApplication("stateless")
var session = stateless.createSession("1")
const runAsync = async () => {
const version = await session.get("/api/version")
console.log(version)
assert(version == "1.0.0")
exit(0)
}
runAsync()
Run the test: node test.js
Expected output:
[motor(1)/17048] GET '/api/version'...
0. execute: ProfilerMiddleware
1. execute: Version Return CString
Done(OK) -- 0.0001213 s
1.0.0
Add the test script in your package.json
:
...
"scripts": {
...
"test:js": "node test.js"
}
It is just the basics of what you can do.
Commit and push your testβ
git add test.js package.json
git commit -m "[test]: js test /api/version"
git push
Use Github Actions for CIβ
Create a github secret:β
- Go to https://github.com/{your-id}/tuto-ws/settings/secrets/actions
- Add a secret name
AZ_PAT
Create a github actionβ
- Go to https://github.com/{your-id}/tuto-ws/actions
- Click on
New workflow
- Select the NodeJS template
- replace the workflow code with the following:
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: ewam CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: windows-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm set //pkgs.dev.azure.com/mphasis-wyde/ewam/_packaging/ewam/npm/registry/:username=VssSessionToken
- run: npm set @ewam:registry=https://pkgs.dev.azure.com/mphasis-wyde/ewam/_packaging/ewam/npm/registry/
- run: npm set @wynsure:registry=https://pkgs.dev.azure.com/mphasis-wyde/ewam/_packaging/ewam/npm/registry/
- run: npm set "//pkgs.dev.azure.com/mphasis-wyde/ewam/_packaging/ewam/npm/registry/:_password=%AZ_PAT%"
env:
AZ_PAT: ${{ secrets.AZ_PAT }}
- run: npm i
- run: npm test
This pipeline is quite standard, what we need to add is a postinstall script to update our TGV:
var { command, file, directory, print } = require("@ewam/script.cli")
const Path = require("path")
function copyTGVs(artifactName) {
command.exec("npm install " + artifactName)
print.info(`copying ${artifactName} tgvs`)
directory.copy(Path.resolve("./node_modules/" + artifactName), Path.resolve("./TGV"))
command.exec("npm uninstall " + artifactName)
}
async function downloadTGVs() {
if (file.exists("./TGV/W001001.TGV")) {
console.log("Leaving TGVs alone as you already have it configured")
} else {
// We can't find any TGV, download it
console.log(
"π₯π₯π₯Downloading TGVsπ₯π₯π₯.... Please grab a β and come back in a couple of minutes depending on your internet speed"
)
try {
copyTGVs("@ewam/tgv-standalone")
command.exec("npm run ewam:patch-system")
// import wam-cloud
command.exec(`npm run ewam:cli -- --import ./node_modules/@ewam/wam-cloud/src --use-exit-code`)
command.exec(`npm run ewam:cli -- --import ./src`)
} catch (e) {
print.error(
"Some errors occured while we tried to download the welcome TGVs", e
)
}
}
}
downloadTGVs()
Add the this script in your package.json:
...
"scripts": {
...
"postinstall": "node postInstall.js"
}
Push your code, it should trigger the pipeline.
Tests using Postman (optional)β
Another approach is to Use Postman/Newman:
- Install newman:
npm i -D newman
- Install postman and create a test on the
/api/version
route and checking it should respond with200
- Export it in a file named:
TestWynsure.postman_collection.json
(in case you used any) - Export your post environment variables in:
wynsure.postman_environment.json
- Add a js script below that will start the server and run newman:
require("./server.js")
const newman = require('newman'); // require newman in your project
const { exit } = require("process")
const runNewman = async (collectionName, reporter) => {
return new Promise((resolve, reject) => {
newman.run({
collection: require(collectionName),
environment: require('./wynsure.postman_environment.json'),
reporters: ['junit', 'cli'],
}, function (err, summary) {
if (err) { reject(err) }
console.log('collection run complete!')
if (summary.error){
reject(summary.error)
}
resolve()
});
})
}
const runTest = async function () {
try {
await runNewman('./TestWynsure.postman_collection.json')
exit(0)
} catch (e){
console.error(e)
exit(1)
}
}
runTest()
git add testWS.js
git commit -m "[test]: postman test for /api/version"
git push
UTF: eWam Unit Test Frameworkβ
We will use an eWam extension that provides unit test utility. See https://github.com/MphasisWyde/ewam-UnitTestFramework
Install the UTF bundle:β
git clone https://github.com/MphasisWyde/ewam-UnitTestFramework.git
node -r dotenv/config ./scripts/installBundle.js --location ./ewam-UnitTestFramework/bundle/WxWAMUnitTestFramework
Add the above command in your postInstall.js
script.
Add the test script in your package.json
:
...
"scripts": {
...
"test:utf": "node ./scripts/call.js ewamconsole.exe /RUN:aUTF_Runner.ConsoleStart /RUNCONTEXTNAME:DefaultRunningContext"
}
=> this command will run the test
Create the testβ
Add the file aTest_aInsuranceWebService.wam
in an src/
folder:
; aTest_aInsuranceWebService (aUTF_ValidationSuites) (Def Version:3) (Implem Version:3)
class aTest_aInsuranceWebService (aUTF_ValidationSuites)
;[TestSpec]
;
procedure Testing_aInsuranceWebService_Version
uses aInsuranceWebService
var TestName : CString
var result : Boolean
var testResult : Boolean
var service : aInsuranceWebService
TestName = 'Testing if the version is correct'
;
result = True
;
new(service)
testResult = service.Version = '0.0.1'
service.CancelObject(service)
;
self.Assert(TestName, testResult)
endProc
Add this test in your config:
[
{"name": "aTest_aInsuranceWebService"}
]
Import the test class:
npm run ewam:cli -- --import ./src/
Finally test your class:
npm test
Check the results in the ./report
folder.
There is a failure, in fact our test is wrong, modify the test to be:
...
testResult = service.Version = '1.0.0'
...
- Run the
npm run ewam:cli -- --import ./src/
- Run the test again:
npm test
- This should be successful now
- Add a github action step to report the result. Use for example https://github.com/marketplace/actions/publish-unit-test-results
git add -A
git commit -m "[test]: UTF for /api/version"
git push
Merge your /api/version branchβ
Go on github on select you PR. Click on Squash and Merge
: All the previous commits are now merge in one single commit inside the master branch.
Return to VSCode and sync up your master branch:
git checkout master
git pull
Feature 2: Implement /api/client routesβ
Complete the class aInsuranceWebService to add 3 functions to:
- Create a client
- Return all the clients
- Retrieve a client
type tNamedObjects : sequence of aNamedObject
;Add a new client
function CreateClient(Name : CString) return aNamedObject
new(_Result)
_Result.Name = Name
_Result.Accept
endFunc
;Retrieve all the existing client
function Clients return tNamedObjects
var client : aNamedObject
forEach client in OQL Select top 10 * from x in aNamedObject++
_Result[-1] = client
endFor
endFunc
;Retrieve a given client
function GetClient(name : CString) return aNamedObject
var client : aNamedObject
forEach client in OQL Select top 10 * from x in aNamedObject++ where x.Name =
name
_Result = client
endFor
endFunc
Mofify InitService
to map the 3 methods to appropriate routes and methods:
function InitService return Boolean
uses aMethodDesc
Net.MapMethod('/version', Net.CreateHttpMethod('[GET]', self, MetaModelEntity(self.Version)))
Net.MapMethod('/clients', Net.CreateHttpMethod('[POST]', self, MetaModelEntity(self.CreateClient)))
Net.MapMethod('/clients/{name}', Net.CreateHttpMethod('[GET]', self, MetaModelEntity(self.GetClient)))
Net.MapMethod('/clients', Net.CreateHttpMethod('[GET]', self, MetaModelEntity(self.Clients)))
endFunc
You can fast create this step via:
move ./tutorials/rest-app/2 ./src
npm run ewam:cli -- --import ./src
Create a feature branchβ
git checkout -b feat/clientAPIs
git add -A
git push
Create a PR
Test the new routesβ
Add tests with the strategy of your to test the new routes.
- Commit your Tests
- Squash and merge your branch like above
Secure your siteβ
You can fast create this step via:
move ./tutorials/rest-app/3 ./src
npm run ewam:cli -- --import ./tutorials/rest-app/3
It is recommended to store any secret with your .env
file. This way it can be easily changed when you deploy your app. Edit .env
to add your API Key within a new env variable.
API_KEY=zaCELgL
Now you have to create a middleware that will be executed before each request and will:
- Read the Authorization header and compare it with your API key
- Return a HTTP Status
401
if the API key is not valid
info
In Wynsure Authentication is done this way as well: a middleware is used to sign in the user.
...
function Login(req : aWebInboundRequest) return aDataDocument
uses Doc, Net
var headers : aDataValue
var authorizationHeader : aDataValue
var error : aDataValue
headers = req.GetAttribut(Doc.Headers)
authorizationHeader = headers.Find('Authorization')
if authorizationHeader.IsDefined and (authorizationHeader.ToCString = wUtil.ExpandEnvVars('(API_KEY)') )
_Result = req.ExecuteNext
else
new(_Result)
Net.SetHTTPStatus(_Result, 401)
error = _Result.GetError
error.Map('error').Map('code').SetCString('UnAuthorized')
error.Map('error').Map('message').SetCString('Please submit a valid API Key')
endIf
endFunc
function InitService return Boolean
uses aMethodDesc
;All routes will be protected
Net.MapMiddleware('/', Net.CreateMiddleware(self, MetaModelEntity(self.Login)))
;
Net.MapMethod('/version', Net.CreateHttpMethod('[GET]', self, MetaModelEntity(self.Version)))
Net.MapMethod('/clients', Net.CreateHttpMethod('[POST]', self, MetaModelEntity(self.CreateClient)))
Net.MapMethod('/clients/{name}', Net.CreateHttpMethod('[GET]', self, MetaModelEntity(self.GetClient)))
Net.MapMethod('/clients', Net.CreateHttpMethod('[GET]', self, MetaModelEntity(self.Clients)))
return true
endFunc
Try it out: Open the url: http://localhost:8080/stateless/api/version
You should receive an error
Try with the Authorization header:
curl -H "Authorization: zaCELgL" http://localhost:8080/stateless/api/version
Pro tip
Have a look at how the error is defined above. It is a best practice to follow a common pattern on error. In general we recommend to follow the microsoft rest api guidelines
Customize the express serverβ
Your eWam application
is hosted by an express server. It means that for some aspect of your work it may be more relevant to directly write your code in javascript.
Edit the file ./server.js
to implement the route /api/version
in javascript. The version should be retrieve by the version defined in your package.json.
Going further
You can combine both above example and implement an Authentication middleware with express! For example you can implement a GitHub SSO with passport and send the user id and password to your application via a header. Ready for it?
Get the documentationβ
Open the url: http://localhost:8080/stateless/documentation
The result is a swagger 3.0 json that can be used with many tools such as SwaggerUI:
Try changing the comment in your APIs, it will update your documentation.
Docker (optional)β
You can also easily dockerize your application:
- Install Docker desktop
docker build -t ewam-tuto .
docker run --name ewam-tuto -p 8080:8080 --net nat ewam-tuto
- Browse http://localhost:8080/stateless/api/clients