Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

better nestjs support #60

Merged
merged 22 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions NESTJS.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
NestJS
=====
With the use of NestJS it's easy to implement an GRPC Microservice ([example](https://docs.nestjs.com/microservices/grpc)). But as you can see you have a lot of boilerplating to define your GRPC interfaces correctly. This approach works fine but is a bit tricky, because it doesn't support type checks. If you change your proto file you also need to update your interface.

By using `ts-proto` you have strong type checks and compiler errors!

To generate `ts` files for your `.proto` files you can use the `--ts_proto_opt=nestJs=true` option.

### Naming convention
For each service in your `.proto` file we generate two `interfaces` on to implement in your nestjs `controller` and one for the `client`.

The name of the `controller` interface is base on the name of the service inside the `.proto`.

If we have to following `.proto` file:
```protobuf
syntax = "proto3";

package hero;

service HeroService {
rpc FindOneHero (HeroById) returns (Hero) {}
rpc FindOneVillain (VillainById) returns (Villain) {}
rpc FindManyVillain (stream VillainById) returns (stream Villain) {}
}
```

The controller interface name would be `HeroServiceController`.
The client interface name would `HeroServiceClient`.

### implementation
To implement the typescript file in your `nestjs` project you need to add the `controller` interface to your controller. We also generate a `decorator` for you controller. For example: `HeroServiceControllerMethods`, when you apply this to your controller we add all the method decorators you normally should do but doing it this way is safer.

For the client we simply pass the `client` interface to the `client.getService<?>();` (see below).

> Note: Based on the `.proto` we'll generate a `const` for example `HERO_PACKAGE_NAME` and `HERO_SERVICE_NAME` this way your code breaks if you change your package or service name. (It's safer to have compiler errors than runtime errors)

##### Controller

```typescript
import { HeroById, Hero, HeroServiceController, VillainById, Villain, HeroServiceControllerMethods } from '../hero';

@Controller('hero')
// Generated decorator that applies all the @GrpcMethod and @GrpcStreamMethod to the right methods
@HeroServiceControllerMethods()
export class HeroController implements HeroServiceController {
private readonly heroes: Hero[] = [
{ id: 1, name: 'Stephenh' },
{ id: 2, name: 'Iangregsondev' }
];

private readonly villains: Villain[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' }
];

async findOneHero(data: HeroById): Promise<Hero> {
return this.heroes.find(({ id }) => id === data.id)!;
}

async findOneVillain(data: VillainById): Promise<Villain> {
return this.villains.find(({ id }) => id === data.id)!;
}

findManyVillain(request: Observable<VillainById>): Observable<Villain> {
const hero$ = new Subject<Villain>();

const onNext = (villainById: VillainById) => {
const item = this.villains.find(({ id }) => id === villainById.id);
hero$.next(item);
};
const onComplete = () => hero$.complete();
request.subscribe(onNext, null, onComplete);

return hero$.asObservable();
}
}
```

##### Client

```typescript
import { HeroById, Hero, HeroServiceController, HeroesService, HERO_SERVICE_NAME, HERO_PACKAGE_NAME } from '../hero';

@Injectable()
export class AppService implements OnModuleInit {
private heroesService: HeroesService;

constructor(@Inject(HERO_PACKAGE_NAME) private client: ClientGrpc) {}

onModuleInit() {
this.heroesService = this.client.getService<HeroesService>(HERO_SERVICE_NAME);
}

getHero(): Observable<Hero> {
return this.heroesService.findOne({ id: 1 });
}
}
```

### Supported options

* With`--ts_proto_opt=addGrpcMetadata=true`, the last argument of service methods will accept the grpc `Metadata` type, which contains additional information with the call (i.e. access tokens/etc.).

(Requires `nestJs=true`.)

* With `--ts_proto_opt=nestJs=true`, the defaults will change to generate [NestJS protobuf](https://docs.nestjs.com/microservices/grpc) friendly types & service interfaces that can be used in both the client-side and server-side of NestJS protobuf implementations.
132 changes: 31 additions & 101 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@
[![npm](https://img.shields.io/npm/v/ts-proto)](https://www.npmjs.com/package/ts-proto)
[![CircleCI](https://circleci.com/gh/stephenh/ts-proto.svg?style=svg)](https://circleci.com/gh/stephenh/ts-proto)

# ts-proto

> `ts-proto` transforms your `.proto` files into strong typed `typescript` files!

## Table of contents

- [QuickStart](#quickstart)
- [Goals](#goals)
- [Example Types](#example-types)
- [Highlights](#highlights)
- [Current Disclaimers](#current-disclaimers)
- [Auto-Batching / N+1 Prevention](#auto-batching---n-1-prevention)
- [Usage](#usage)
+ [Supported options](#supported-options)
+ [Only Types](#only-types)
+ [NestJS Support](NESTJS.markdown)
- [Building](#building)
- [Assumptions](#assumptions)
- [Todo](#todo)
- [Typing Approach](#typing-approach)
- [OneOf Handling](#oneof-handling)
- [Primitive Types](#primitive-types)
- [Wrapper Types](#wrapper-types)
- [Number Types](#number-types)
- [Current Status of Optional Values](#current-status-of-optional-values)

Overview
========

Expand Down Expand Up @@ -53,105 +79,6 @@ This will generate `*.ts` source files for the given `*.proto` types.

If you want to package these source files into an npm package to distribute to clients, just run `tsc` on them as usual to generate the `.js`/`.d.ts` files, and deploy the output as a regular npm package.

NestJS
=====
With the use of NestJS it's easy to implement an GRPC Microservice ([example](https://docs.nestjs.com/microservices/grpc)). But as you can see you have a lot of boilerplating to define your GRPC interfaces correctly. This approach works fine but is a bit tricky, because it doesn't support type checks. If you change your proto file you also need to update your interface.

By using `ts-proto` you have strong type checks and compiler errors!

To generate `ts` files for your `.proto` files you can use the `--ts_proto_opt=nestJs=true` option.

### Naming convention
For each service in your `.proto` file we generate two `interfaces` on to implement in your nestjs `controller` and one for the `client`.

The name of the `controller` interface is base on the name of the service inside the `.proto`.

If we have to following `.proto` file:
```protobuf
syntax = "proto3";

package hero;

service HeroService {
rpc FindOneHero (HeroById) returns (Hero) {}
rpc FindOneVillain (VillainById) returns (Villain) {}
rpc FindManyVillain (stream VillainById) returns (stream Villain) {}
}
```

The controller interface name would be `HeroServiceController`.
The client interface name would `HeroServiceClient`.

### implementation
To implement the typescript file in your `nestjs` project you need to add the `controller` interface to your controller. We also generate a `decorator` for you controller. For example: `HeroServiceControllerMethods`, when you apply this to your controller we add all the method decorators you normally should do but doing it this way is safer.

For the client we simply pass the `client` interface to the `client.getService<?>();`.

> Note: Based on the `.proto` we'll generate a `const` for example `HERO_PACKAGE_NAME` and `HERO_SERVICE_NAME` this way your code breaks if you change your package or service name. (It's safer to have compiler errors than runtime errors)

##### Controller

```typescript
import { HeroById, Hero, HeroServiceController, VillainById, Villain, HeroServiceControllerMethods } from '../hero';

@Controller('hero')
// Generated decorator that applies all the @GrpcMethod and @GrpcStreamMethod to the right methods
@HeroServiceControllerMethods()
export class HeroController implements HeroServiceController {
private readonly heroes: Hero[] = [
{ id: 1, name: 'Stephenh' },
{ id: 2, name: 'Iangregsondev' }
];

private readonly villains: Villain[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' }
];

async findOneHero(data: HeroById): Promise<Hero> {
return this.heroes.find(({ id }) => id === data.id)!;
}

async findOneVillain(data: VillainById): Promise<Villain> {
return this.villains.find(({ id }) => id === data.id)!;
}

findManyVillain(request: Observable<VillainById>): Observable<Villain> {
const hero$ = new Subject<Villain>();

const onNext = (villainById: VillainById) => {
const item = this.villains.find(({ id }) => id === villainById.id);
hero$.next(item);
};
const onComplete = () => hero$.complete();
request.subscribe(onNext, null, onComplete);

return hero$.asObservable();
}
}
```

##### Client

```typescript
import { HeroById, Hero, HeroServiceController, HeroesService, HERO_SERVICE_NAME, HERO_PACKAGE_NAME } from '../hero';

@Injectable()
export class AppService implements OnModuleInit {
private heroesService: HeroesService;

constructor(@Inject(HERO_PACKAGE_NAME) private client: ClientGrpc) {}

onModuleInit() {
this.heroesService = this.client.getService<HeroesService>(HERO_SERVICE_NAME);
}

getHero(): Observable<Hero> {
return this.heroesService.findOne({ id: 1 });
}
}
```

Goals
=====

Expand Down Expand Up @@ -304,18 +231,21 @@ protoc --plugin=node_modules/ts-proto/protoc-gen-ts_proto ./batching.proto -I.

(Requires `nestJs=true`.)

* With `--ts_proto_opt=nestJs=true`, the defaults will change to generate [NestJS protobuf](https://docs.nestjs.com/microservices/grpc) friendly types & service interfaces that can be used in both the client-side and server-side of NestJS protobuf implementations.
* With `--ts_proto_opt=nestJs=true`, the defaults will change to generate [NestJS protobuf](https://docs.nestjs.com/microservices/grpc) friendly types & service interfaces that can be used in both the client-side and server-side of NestJS protobuf implementations. See the [nestjs readme](NESTJS.markdown) for more information and implementation examples.

Specifically `outputEncodeMethods`, `outputJsonMethods`, and `outputClientImpl` will all be false, and `lowerCaseServiceMethods` will be true.

Note that `addGrpcMetadata` and `returnObservable` will still be false.

### "Only Types"
### Only Types

If you're looking for `ts-proto` to generate only types for your Protobuf types then passing all three of `outputEncodeMethods`, `outputJsonMethods`, and `outputClientImpl` as `false` is probably what you want, i.e.:

`--ts_proto_opt=outputEncodeMethods=false,outputJsonMethods=false,outputClientImpl=false`.

### NestJS Support
We have a great way of working together with [nestjs](https://docs.nestjs.com/microservices/grpc). `ts-proto` generates `interfaces` and `decorators` for you controller, client. For more information see the [nestjs readme](NESTJS.markdown).

Building
========

Expand Down
16 changes: 16 additions & 0 deletions integration/nestjs-simple/google/protobuf/empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

/**
* A generic empty message that you can re-use to avoid defining duplicated
* empty messages in your APIs. A typical example is to use it as the request
* or the response type of an API method. For instance:
*
* service Foo {
* rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
* }
*
* The JSON representation for `Empty` is empty JSON object `{}`.
*/
export interface Empty {
}

export const GOOGLE.PROTOBUF_PACKAGE_NAME = 'google.protobuf'
Binary file modified integration/nestjs-simple/hero.bin
Binary file not shown.
3 changes: 3 additions & 0 deletions integration/nestjs-simple/hero.proto
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
syntax = "proto3";

import "google/protobuf/empty.proto";

package hero;

service HeroService {
rpc AddOneHero (Hero) returns (google.protobuf.Empty) {}
rpc FindOneHero (HeroById) returns (Hero) {}
rpc FindOneVillain (VillainById) returns (Villain) {}
rpc FindManyVillain (stream VillainById) returns (stream Villain) {}
Expand Down
7 changes: 6 additions & 1 deletion integration/nestjs-simple/hero.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Observable } from 'rxjs';
import { Empty } from './google/protobuf/empty';
import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices';


Expand All @@ -22,6 +23,8 @@ export interface Villain {

export interface HeroServiceController {

addOneHero(request: Hero): void;

findOneHero(request: HeroById): Promise<Hero> | Observable<Hero> | Hero;

findOneVillain(request: VillainById): Promise<Villain> | Observable<Villain> | Villain;
Expand All @@ -32,6 +35,8 @@ export interface HeroServiceController {

export interface HeroServiceClient {

addOneHero(request: Hero): Observable<Empty>;

findOneHero(request: HeroById): Observable<Hero>;

findOneVillain(request: VillainById): Observable<Villain>;
Expand All @@ -42,7 +47,7 @@ export interface HeroServiceClient {

export function HeroServiceControllerMethods() {
return function (constructor: Function) {
const grpcMethods: string[] = ['findOneHero', 'findOneVillain'];
const grpcMethods: string[] = ['addOneHero', 'findOneHero', 'findOneVillain'];
for (const method of grpcMethods) {
const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
GrpcMethod('HeroService', method)(constructor.prototype[method], method, descriptor);
Expand Down
4 changes: 4 additions & 0 deletions integration/nestjs-simple/nestjs-project/hero.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export class HeroController implements HeroServiceController {
{ id: 2, name: 'Doe' }
];

addOneHero(request: Hero) {
this.heroes.push(request);
}

async findOneHero(data: HeroById): Promise<Hero> {
return this.heroes.find(({ id }) => id === data.id)!;
}
Expand Down
6 changes: 6 additions & 0 deletions integration/nestjs-simple/nestjs-simple-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ describe('nestjs-simple-test nestjs', () => {
expect(heroService).not.toBeUndefined();
});

it('should addOneHero', async () => {
const emptyResponse = await heroService.addOneHero({ id: 3, name: 'Toon' }).toPromise();
expect(emptyResponse).toEqual({});
});


it('should findOneHero', async () => {
const hero = await heroService.findOneHero({ id: 1 }).toPromise();
expect(hero).toEqual({ id: 1, name: 'Stephenh' });
Expand Down
3 changes: 3 additions & 0 deletions integration/nestjs-simple/sample-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { HeroServiceController, HeroById, Hero, Villain, VillainById } from './h
import { Observable, Subject } from 'rxjs';

export class SampleService implements HeroServiceController {

addOneHero(request: Hero): void {}

findOneHero(request: HeroById): Promise<Hero> {
return Promise.resolve({ id: 1, name: 'test' });
}
Expand Down
Loading