Skip to main content

Publication tutorial

Difficulty: Medium

Pre-requisite​

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 version
  • Post /api/clients => to create a specific client
  • Get /api/clients=> To retrieve all the clients
  • Get /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.

ResourcePOSTGETPUTDELETE
/customersCreate a new customerRetrieve all customersBulk update of customersRemove all customers
/customers/1ErrorRetrieve the details for customer 1Update the details of customer 1 if it existsRemove customer 1
/customers/1/ordersCreate a new order for customer 1Retrieve all orders for customer 1Bulk update of orders for customer 1Remove 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:

  1. Create a service class
  2. Declare it in the configuration file
  3. Protect it via middlewares
  4. 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"
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:

./src/aInsuranceWebService.wam
; 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:

./Admin/config-runtime.json
{
"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

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:

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:​

Create a github action​

node.js.yml
# 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:

postInstall.js
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:

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 with 200
  • 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:
testWS.js
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:

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:

./src/aTest_aInsuranceWebService.wam
; 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:

./Admin/unit-test-config.json
[
{"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:

./src/aTest_aInsuranceWebService.wam
...
testResult = service.Version = '1.0.0'
...
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.

aInsuranceWebService
...
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:

The fallback content to display on prerendering

Try changing the comment in your APIs, it will update your documentation.

Docker (optional)​

You can also easily dockerize your application: