Auto Client API

This feature comes from a common use of the Node.js BAL where service writers create a client module that other services can use to more easily make bus calls to their service. These client modules tend to duplicate a lot of code that could be easily abstracted away into a single automatically generated client API.

Creating a Client API

  • bal.client: (serviceType: String, defaultRealm: String, serviceVersion: Number) => Prototype

Client APIs are created by defining a prototype with a set of operations that will be available to users. When a prototype is initialized by the user, those operation definitions provide a client API that can be used to interact with specific services more easily. With a good client API, users of your service can avoid simple mistakes that occur during direct Bus calls and will be somewhat shielded from deprecations and API changes in the future.

The prototype file can be distributed by simply exporting the initialization function, along with whatever else is needed to support the API (constants, etc.).

1
2
3
4
5
6
7
8
9
10
11
// <calculator_prototype.js>

var bal = require('proxy-bal');
var prototype = bal.client("avid.acs.calculator", "global", 3);

// API definitions...

module.exports = {
init: prototype.init,
const: ...
}

Automatic Operations

  • prototype.auto: (operationName: String) => Prototype
  • prototype.auto: (operationNames: Array<String>) => Prototype

Within our prototype definition, we can automatically generate operations from a list of names using prototype.auto:

1
2
3
4
5
// All at once,
prototype.auto(['add', 'subtract', 'multiply', 'divide']);

// Or at our leisure
prototype.auto('add').auto(['subtract', 'multiply']).auto('divide');

These operations will be automatically implemented on an initialized client as both client.op and client._op and will have the following signature:

1
2
3
4
5
op(callback)
op(paramSet, callback)
op(paramSet, context, callback)

where callback takes (errorSet, resultSet, context)

For example, to send an add request with the automatic operation, a user could do something like this:

1
2
3
4
client.add({ num1: 2, num2: 3}, function (errs, results, context) {
// ...
console.log("The sum is ", results.sum);
})

Redefining Operations

  • prototype.op: (opName: String, opDefinition: Function) => Prototype
  • prototype.ops: (opDefinitions: Map<String, Function>) => Prototype

For common operations, it is preferable to provide a cleaner definition so that users can interact with your API more smoothly. To redefine or create custom operations, we can use prototype.ops.

1
2
3
4
5
6
7
8
9
10
11
prototype.ops({
add: function (num1, num2, callback) {
// Call the automatically implemented op.
this._add({ num1: num1, num2: num2 }, function (errs, results) {
if (errs && errs.length > 0) return callback(errs);

// Just return the sum.
return callback(null, results.sum);
})
}
})

Here we redefine the add operation so that it takes two arguments and a callback. Note that we can still use the automatically generated operation. We have redefined add, but _add will remain unaffected.

We also take the opportunity to return only the relevant data to the user (the sum). As a result, clients can use our API much more easily:

1
2
3
4
client.add(2, 3, function (errs, sum) {
// ...
console.log("The sum is ", sum);
})

Initializing a Client

  • prototype.init: (access: Access, [options]: Map) => Client
  • prototype.initFromServiceInfo: (access: Access, [options]: Map, callback: Function) => Void

Users can create a client by calling the prototype.init function. This function takes a bal.Access instance to send queries with as well as a set of options:

1
2
3
4
5
6
7
8
9
10
var client = prototype.init(access, {
// Override the default realm and use this one instead.
realm: "acs.test",

// Set the timeout for queries sent with this client.
timeout: 5000,

// Additional custom options can also be added...
myOption: true
})

The options passed here will also be available to custom operations through the this.options variable.

It is also possible to initialize a client and populate the operation data by requesting the service info of that service:

1
2
3
4
5
6
7
8
prototype.initFromServiceInfo(access, function (errs, client) {
// During initialization, the service info will be requested and
// then used to automatically generate ops. Any errors from that
// request will be in the 'errs' array.

// If succesful, we can now start using our 'client' object.
// ...
});

Changing Default Context

  • client.withContext: (context: Object) => Client

It is possible to set the default context sent by a client using the withContext function. This will return a new client object that adds the passed context to every message it sends.

1
var authorizedClient = client.withContext({ identity: token });

This context can be overridden on an individual call basis as well:

1
2
3
4
5
6
7
authorizedClient.add({}, { id: 2 }, function (errs, results) { /* ... */});

// Sends with context:
// {
// identity: token,
// id: 2
// }

Default Operations

Certain common service operations are automatically provided.

  • client.echo: (callback: Function) => Void
1
2
3
4
5
var client = bal.client("avid.acs.calculator", "global", 3).init(access);

client.echo(function (errs, results) {
console.log("Echo response is", results);
});
  • client.serviceInfo: (callback: Function) => Void
1
2
3
4
5
var client = bal.client("avid.acs.calculator", "global", 3).init(access);

client.serviceInfo(function (errs, info) {
console.log("Service info is", info);
});
  • client.serviceStatus: (callback: Function) => Void
1
2
3
4
5
var client = bal.client("avid.acs.calculator", "global", 3).init(access);

client.serviceStatus(function (errs, status) {
console.log("Service status is", status);
});

Full Calculator Prototype Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var bal = require('proxy-bal');
var prototype = module.exports = bal.client("avid.acs.calculator", "global", 3);

prototype.auto(['add', 'subtract', 'multiply', 'divide']);

var binaryOp = function (op, resultName) {
return function (num1, num2, callback) {
this[op]({ num1: num1, num2: num2 }, function (errs, results) {
if (errs && errs.length > 0) return callback(errs);
return callback(null, results[resultName]);
});
};
};

prototype.ops({
add: binaryOp('_add', 'sum'),
subtract: binaryOp('_subtract', 'difference'),
multiply: binaryOp('_multiply', 'product'),
divide: binaryOp('_divide', 'quotient')
});