@pawsteam/ts-jsonrpc-server
Advanced tools
Comparing version 0.2.0 to 0.2.1
{ | ||
"name": "@pawsteam/ts-jsonrpc-server", | ||
"version": "0.2.0", | ||
"version": "0.2.1", | ||
"description": "A framework for easily building JSONRPC simple servers in TS", | ||
@@ -5,0 +5,0 @@ "main": "dist/server.js", |
237
README.md
@@ -22,3 +22,3 @@ # Typescript JSONRPC server | ||
port: 2000, | ||
services: [SomeService], | ||
services: [HotelsService], | ||
genericValidators: [ | ||
@@ -34,3 +34,3 @@ new HeaderExistsValidator('content-type'), | ||
class HotelsService { | ||
listHotels(): Promise<any> { | ||
listHotels(request: IncomingMessage, response: ServerResponse): Promise<any> { | ||
return new Promise((resolve, reject) => { | ||
@@ -44,3 +44,8 @@ // fetch data.then((data) => { | ||
``` | ||
Observe how we have an array, `services: [HotelsService]` which uses the class with the same name and listens on the path `/hotels`. | ||
By default, the procedure that gets called receives 2 parameters: a request object of type `IncomingMessage`, default for nodejs HTTP servers, and a response object of type `ServerResponse`, also default for nodejs HTTP servers. | ||
The method you want to export must return a promise for any type of data. | ||
Later, when running this app, you can make a JSONRPC request to the methods exposed by this endpoint: | ||
@@ -79,32 +84,38 @@ | ||
They check the request for required data and types. It's a place to define what parameters are needed for a method to be called. | ||
Any number of Input Validators can be assigned for one method. Generic ones as well; | ||
#### Generic validators | ||
Generic validators are set on the app level, and they will be triggered on all requests for this app. | ||
```typescript | ||
@AppConfigDecorator({ | ||
port: 2000, | ||
services: [SomeService], | ||
genericValidators: [ | ||
new HeaderExistsValidator('content-type'), | ||
new HeaderValueContainsValidator('user-agent', 'Mozilla/5.0') | ||
] | ||
}) | ||
export class AppMain { | ||
They check the request for required data, without which you cannot perform your business logic. It's a place to define what parameters are needed for a method to be called. | ||
Any number of Input Validators can be assigned for one method. Generic ones as well; | ||
An input validator must implement the `ValidatorInterface`, which means it must implement the `validate` method, which will receive 2 parameters: `params` which is a JSON object with the params from the JSONRPC object. The second parameter is the whole request object, of type `IncomingMessage` | ||
This `validate` method returns true of false. A truthy value will make the validation pass, a falsy value will make the validation fail. | ||
Passing a validation means the exported method will be called. | ||
Failing a validation means a `400 Bad Request` will be returned. | ||
#### Generic validators | ||
Generic validators are set on the app level, and they will be triggered on all requests for this app. | ||
```typescript | ||
@AppConfigDecorator({ | ||
port: 2000, | ||
services: [SomeService], | ||
genericValidators: [ | ||
new HeaderExistsValidator('content-type'), | ||
new HeaderValueContainsValidator('user-agent', 'Mozilla/5.0') | ||
] | ||
}) | ||
export class AppMain { | ||
} | ||
export class HeaderExistsValidator implements ValidatorInterface { | ||
headerName: string; | ||
constructor(headerName: string) { | ||
this.headerName = headerName; | ||
} | ||
export class HeaderExistsValidator implements ValidatorInterface { | ||
headerName: string; | ||
constructor(headerName: string) { | ||
this.headerName = headerName; | ||
} | ||
validate(params: any, request): boolean { | ||
return !!request.headers[this.headerName]; | ||
} | ||
validate(params: any, request: IncomingMessage): boolean { | ||
return !!request.headers[this.headerName]; | ||
} | ||
``` | ||
The input validators implement ValidatorInterface and return a boolean value. | ||
} | ||
``` | ||
@@ -124,42 +135,78 @@ #### Method specific validators | ||
They transform the request data into objects that are passed to the methods. Any number of input transformer can exist per method. | ||
They transform the request data into application-defined objects that are passed to the methods. Any number of input transformers can exist per method. | ||
They must implement the `TransformerInterface`, which means implementing the method `transform` which receives 2 parameters. | ||
First one is the default request, of type `IncomingMessage` and the second one is the parsed JSON from the JSONRPC call, of type `ParsedRpcObjectType` | ||
The transformers return a Promise of object type required. | ||
Examples below: | ||
1. The transformer returns a promise of one parameter type. | ||
2. The order of validators will give the order of parameters in the method call. | ||
3. The transformers implement the TransformerInterface and return a Promise to the type of object they are for. | ||
``` | ||
export class BucketTransformer implements TransformerInterface { | ||
/** | ||
* The ParsedRpcObjectType is an object created from the body of the request, parsed, | ||
* and put into the form of a JSONRPC object: | ||
* { | ||
* id: <id>, | ||
* method: <methodname>, | ||
* jsonrpc: '2.0', | ||
* params: {...} | ||
* } | ||
*/ | ||
transform(request: IncomingMessage, jsonrpcObj: ParsedRpcObjectType): Promise<BucketType> { | ||
const newBucket = new BucketType(jsonrpcObj.params.path); | ||
return Promise.resolve(newBucket); | ||
} | ||
```typescript | ||
export class BucketTransformer implements TransformerInterface { | ||
/** | ||
* The ParsedRpcObjectType is an object created from the body of the request, parsed, | ||
* and put into the form of a JSONRPC object: | ||
* { | ||
* id: <id>, | ||
* method: <methodname>, | ||
* jsonrpc: '2.0', | ||
* params: {...} | ||
* } | ||
*/ | ||
transform(request: IncomingMessage, jsonrpcObj: ParsedRpcObjectType): Promise<BucketType> { | ||
const newBucket = new BucketType(jsonrpcObj.params.path); | ||
return Promise.resolve(newBucket); | ||
} | ||
``` | ||
} | ||
``` | ||
The reason this method needs to return a promise is because we might need to fetch additional data in order to create the object. | ||
For instance, we might need to query a DB. | ||
The reason this method needs to return a promise is because we might need to fetch additional data in order to create the object. | ||
For instance, we might need to query a DB. | ||
Validation can also be done in the transformer. If the transformer rejects the promise or throws, the transformation is interpreted as failed, so validation failed, returning `400 Bad Request`. | ||
To use a transformer, you simply instantiate a new instance of your transformer: | ||
Validation can also be done in the transformer. If the transformer rejects the promise or throws, the transformation is interpreted as failed, so validation failed. | ||
To use a transformer, you simply instantiate a new instance of your transformer: | ||
``` | ||
@TransformerDecorator(new CarTransformer()) | ||
@TransformerDecorator(new EngineTransformer()) | ||
method3(car: CarType, engine: EngineType, request: IncomingMessage, response: ServerResponse) { | ||
return Promise.resolve(car.wheels * engine.power); | ||
```typescript | ||
class CarType { | ||
wheels: number; | ||
engine: string; | ||
constructor(_wheels: number, _engine: string) { | ||
this.wheels = _wheels; | ||
this.engine = _engine; | ||
} | ||
``` | ||
} | ||
class EngineType { | ||
power: number | ||
constructor(_power: number) { | ||
this.power = _power; | ||
} | ||
} | ||
class CarTransformer implements TransformerInterface { | ||
transform(request: IncomingMessage, jsonrpcObj: ParsedRpcObjectType): Promise<any> { | ||
return Promise.resolve(new CarType(4, 'v6')); | ||
} | ||
} | ||
class EngineTransformer implements TransformerInterface { | ||
transform(request: IncomingMessage, jsonrpcObj: ParsedRpcObjectType): Promise<any> { | ||
return new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
resolve(new EngineType(260)); | ||
}, 500); | ||
}) | ||
} | ||
} | ||
@TransformerDecorator(new CarTransformer()) | ||
@TransformerDecorator(new EngineTransformer()) | ||
method3(car: CarType, engine: EngineType, request: IncomingMessage, response: ServerResponse) { | ||
return Promise.resolve(car.wheels * engine.power); | ||
} | ||
``` | ||
Note how we instantiate the 2 classes for transforming, CarTransformer and EngineTransformer, and how the order of the two decorators is kept in the parameters passed to the method. | ||
The first transformer will pass the first parameter, and the second transformer passes the second parameter. | ||
Last 2 parameters remain the request and response. | ||
### The method itself | ||
@@ -169,18 +216,31 @@ | ||
It must also return a promise of any type of object, this object will be serialized/processed if needed, then replied to the user | ||
It must also return a promise of any type of object, this object will be serialized/processed if needed, then replied to the user | ||
``` | ||
@ValidatorDecorator(new BucketValidator()) | ||
@TransformerDecorator(new BucketTransformer()) | ||
createBucket(bucket: BucketType, context: RequestContextType): Promise<any> { | ||
return Promise.resolve({created: bucket.path}); | ||
} | ||
``` | ||
```typescript | ||
@ValidatorDecorator(new BucketValidator()) | ||
@TransformerDecorator(new BucketTransformer()) | ||
createBucket(bucket: BucketType, context: RequestContextType): Promise<any> { | ||
return Promise.resolve({created: bucket.path}); | ||
} | ||
``` | ||
As a best practice, the validators should be as light as possible, and the validation should be avoided in the transformers, unless absolutely necessary. | ||
### Response serializer | ||
This is where you can decide what exactly ends up in the response. Everything in the response can be changed at this step. | ||
The serializers must implement **SerializerInterface**. | ||
They must return an object of type JsonRpcResponseType. | ||
The serializers must implement `SerializerInterface`, which means implement the method `serialize`. This method receives 1 parameter of type `SerializerParameterType` which is a JSON containing a lot of information. | ||
The properties of this object are: | ||
```typescript | ||
public objectToBeReturned: JsonRpcResponseType; | ||
public methodOutput: HttpResponse; | ||
public requestJsonRpcBody: ParsedRpcObjectType; | ||
public originalRequest: IncomingMessage; | ||
public service: ServiceType<any>; | ||
``` | ||
Basically all of the information that are available in this request are passed through this object. | ||
They must return an object of type JsonRpcResponseType (Which is also passed as a parameter). | ||
```typescript | ||
@@ -203,4 +263,15 @@ // example of serializer - removes all keys that contain the string 'test' from the response. | ||
} | ||
``` | ||
``` | ||
Expected output: | ||
```json | ||
{ | ||
"id": ..., | ||
"jsonrpc": "2.0", | ||
"result": { | ||
"someKey": 16 | ||
} | ||
} | ||
``` | ||
## Events | ||
@@ -217,5 +288,5 @@ | ||
```typescript | ||
// step 1 - define the connection & transport | ||
// for now only Rabbit transporters are allowed | ||
@AppConfigDecorator({ | ||
// step 1 - define the connection & transport | ||
// for now only Rabbit transporters are allowed | ||
messaging: { | ||
@@ -233,3 +304,2 @@ serviceName: 'NewService', | ||
```typescript | ||
// step 2 - in the service method, mention the decorator | ||
@ServiceDecorator({ | ||
@@ -240,3 +310,4 @@ path: '/newpath', | ||
export class NewService implements ServiceInterface { | ||
EventEmitterDecorator() | ||
// step 2 - in the service method, mention the decorator | ||
@EventEmitterDecorator() | ||
method2() { | ||
@@ -247,3 +318,3 @@ return Promise.resolve({sum: 8, product: 15}); | ||
``` | ||
By default, this will render a message similar to this | ||
By default, this will produce an event with the full response object as `eventParams` | ||
```typescript | ||
@@ -259,7 +330,7 @@ { | ||
Alternatively, you can also select and change the keys that will be published in the event, by passing a json parameter to the EventEmitterDecorator() | ||
Alternatively, you can also filter the keys that will be published in the event, and even rename them, by passing a json parameter to the EventEmitterDecorator() | ||
```typescript | ||
@EventEmitterDecorator({ | ||
sum: '', // an empty string means the key's name will be used in the event keys | ||
sum: '', // an empty string means the key's original name will be used in the event keys | ||
prod: 'product' | ||
@@ -281,3 +352,3 @@ }) | ||
Notice how the returned keys are _sum, prod and excluded_, and the event only has _sum and product_. | ||
Notice how the returned keys are `sum`, `prod` and `excluded`, and the event only has `sum` and `product`, where the `product` is actually the returned `prod`. | ||
50003
342