Skip to content

Commit

Permalink
Version 0.8.0
Browse files Browse the repository at this point in the history
  • Loading branch information
codenothing committed Nov 13, 2022
0 parents commit 2066c2d
Show file tree
Hide file tree
Showing 26 changed files with 8,732 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
coverage
test/client
10 changes: 10 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
coverage/
dist/
test/client/
7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2022 Corey Hart

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
224 changes: 224 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# ProtoClient

A typed gRPC Client with static code generation

## Static Code Generation

Translates `.proto` files into functional API calls

```
proto-client [options] file1.proto file2.proto ...
Options:
--version Show version number
-o, --output Output directory for compiled files
-p, --path Adds a directory to the include path
--keep-case Keeps field casing instead of converting to camel case
--force-long Enforces the use of 'Long' for s-/u-/int64 and s-/fixed64 fields
--force-number Enforces the use of 'number' for s-/u-/int64 and s-/fixed64 fields
--help Show help
```

### Example

Given the following `animals.proto` file, running the `proto-client` command will generate a js/ts client scaffold for easy functional request making

```proto
syntax = "proto3";
package animals;
message Animal {
string id = 1;
string name = 2;
string action = 3;
}
message AnimalRequest {
string name = 1;
}
message CreateAnimalRequest {
string name = 1;
string action = 2;
}
service AnimalService {
rpc GetAnimal (AnimalRequest) returns (Animal);
rpc CreateAnimal (stream CreateAnimalRequest) returns (stream Animal);
}
```

The `client.js` code generated:

```js
module.exports.animals = {
AnimalService: {
GetAnimal: async (data) =>
protoClient.makeUnaryRequest("animals.AnimalService.GetAnimal", data),

CreateAnimal: async (writerSandbox, streamReader) =>
protoClient.CreateAnimal(
"animals.AnimalService.CreateAnimal",
writerSandbox,
streamReader
),
},
};
```

To make calls:

```ts
import { protoClient, animals } from "output/client";

// Configuring service endpoint(s)
protoClient.configureClient({ endpoint: "127.0.0.1:8080" });

// Unary Requests
const request = await animals.AnimalService.GetAnimal({ name: "dog" });
request.result; // Animal

// Bidrectional Requests
await animals.AnimalService.CreateAnimal(
async (write) => {
await write({ name: "dog", action: "bark" });
await write({ name: "cat", action: "meow" });
},
async (row) => {
// ... each streamed row ...
}
);
```

## Middleware

The `protoClient` instance provides middleware injection to adjust requests before they are sent. A great use case would be adding authentication tokens to the request metadata in one location rather than at each line of code a request is made

```ts
protoClient.useMiddleware((request) => {
request.metadata.set("authToken", "abc_123");
});
```

## Events

Each request instance extends NodeJS's [EventEmitter](https://nodejs.org/api/events.html#class-eventemitter), allowing callers to hook into specific signal points during the process of a request. To extend the use case above, updates to authentication tokens can be passed back in Metadata and auto assigned in one location rather than at each line of code a request is made

```ts
let authToken = "";

protoClient.useMiddleware((request) => {
if (authToken) {
request.metadata.set("authToken", authToken);
}

request.on("response", () => {
const token = request.responseMetadata?.get("updatedAuthToken");
if (token) {
authToken = token[0];
}
});
});
```

### List of Events

- **ProtoRequest**:
- `response`: Event emitted right before the first response is sent to the caller
- `retry`: Event emitted right before a retry request is started
- `aborted`: Event emitted when the request has been aborted
- `end`: Event emitted after the last response (or error) has been returned to the caller
- **ProtoClient**:
- `request`: Event emitted before a request is started, but after all middleware has run. (_will not emit if middleware throws an error_)

## Multi Service Support

For multi service architectures, `ProtoClient` comes with built-in support to configure which requests point to with service endpoint using a matching utility. This is purely configuration, and requires no changes to individual methods/requests

```ts
protoClient.configureClient({
endpoint: [
// Matching all rpc methods of the animals.AnimalService to a specific service endpoint
{
address: "127.0.0.1:8081",
match: "animals.AnimalService.*",
},

// Matching all rpc methods of the transportation.VehicleService to a specific service endpoint
{
address: "127.0.0.1:8082",
match: "transportation.VehicleService.*",
},

// Matching all entire namespace to a specific service endpoint
{
address: "127.0.0.1:8083",
match: "veterinarian.*",
},
],
});
```

## Retry Options

Every request is wrapped in a retry handle allowing for fully customized handling timeouts and retries. Can be configured at both the overall client and individual request levels.

- `timeout`: Time in milliseconds before cancelling the request. Defaults to 0 for no timeout
- `retryOptions`: Retry configuration object
- `retryCount`: Number of times to retry request. Defaults to none
- `status`: Status codes request is allowed to retry on. Uses default list of status codes if not defined
- `retryOnClientTimeout`: Indicates if retries should be allowed when the client times out. Defaults to true.

## Request API

The underneath `ProtoClient` class covers all four gRPC request methods

#### makeUnaryRequest

Making unary requests to a gRPC endpoint.

```ts
function makeUnaryRequest<RequestType, ResponseType>(
method: string,
data: RequestType,
requestOptions?: RequestOptions
): Promise<ProtoRequest>;
```

#### makeClientStreamRequest

Making streamed requests to a gRPC endpoint.

```ts
function makeClientStreamRequest<RequestType, ResponseType>(
method: string,
writerSandbox: WriterSandbox<RequestType, ResponseType>,
requestOptions?: RequestOptions
): Promise<ProtoRequest>;
```

#### makeServerStreamRequest

Making requests to a gRPC endpoint while streaming the response.

```ts
function makeServerStreamRequest<RequestType, ResponseType>(
method: string,
data: RequestType,
streamReader: streamReader<RequestType, ResponseType>,
requestOptions?: RequestOptions
): Promise<ProtoRequest>;
```

#### makeBidiStreamRequest

Making bidirectional requests to a gRPC endpoint, streaming data to and from the service.

```ts
function makeBidiStreamRequest<RequestType, ResponseType>(
method: string,
writerSandbox: WriterSandbox<RequestType, ResponseType>,
streamReader: streamReader<RequestType, ResponseType>,
requestOptions?: RequestOptions
): Promise<ProtoRequest>;
```
2 changes: 2 additions & 0 deletions bin/proto-client
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require("proto-client/dist/cli/cli.js").cli();
10 changes: 10 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
roots: ["<rootDir>/test"],
transform: {
"^.+\\.ts$": "ts-jest",
},
testEnvironment: "node",
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.ts$",
moduleFileExtensions: ["ts", "js", "json", "node"],
setupFilesAfterEnv: ["<rootDir>/test/setup.ts"],
};
55 changes: 55 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "proto-client",
"version": "0.8.0",
"description": "A typed gRPC Client with static code generation",
"author": "Corey Hart <[email protected]>",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": "./bin/proto-client",
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/codenothing/proto-client.git"
},
"scripts": {
"clean": "rm -rf dist",
"build": "yarn clean && tsc -p tsconfig.json",
"lint": "eslint . --ext .ts",
"pretest": "yarn build && yarn lint",
"test": "jest --verbose --coverage",
"prepublish": "yarn test"
},
"keywords": [
"proto",
"protobuf",
"grpc",
"client"
],
"files": [
"dist",
"bin",
"package.json",
"README.md",
"LICENSE"
],
"dependencies": {
"@grpc/grpc-js": "^1.7.3",
"@types/cli-color": "^2.0.2",
"@types/yargs": "^17.0.13",
"cli-color": "^2.0.3",
"protobufjs": "^7.1.2",
"protobufjs-cli": "^1.0.2",
"yargs": "^17.6.2"
},
"devDependencies": {
"@grpc/proto-loader": "^0.7.3",
"@types/jest": "^29.2.2",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
"eslint": "^8.27.0",
"jest": "^29.3.0",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4"
}
}
Loading

0 comments on commit 2066c2d

Please sign in to comment.