Skip to content

Commit

Permalink
fix: Allow other services with nestJs.
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenh committed Dec 30, 2023
1 parent ec8579d commit 67f3019
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 47 deletions.
205 changes: 204 additions & 1 deletion integration/nestjs-metadata-grpc-js/hero.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable */
import { Metadata } from "@grpc/grpc-js";
import { handleBidiStreamingCall, Metadata } from "@grpc/grpc-js";
import type { handleUnaryCall, UntypedServiceImplementation } from "@grpc/grpc-js";
import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices";
import * as _m0 from "protobufjs/minimal";
import { Observable } from "rxjs";

export const protobufPackage = "hero";
Expand All @@ -25,6 +27,170 @@ export interface Villain {

export const HERO_PACKAGE_NAME = "hero";

function createBaseHeroById(): HeroById {
return { id: 0 };
}

export const HeroById = {
encode(message: HeroById, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== 0) {
writer.uint32(8).int32(message.id);
}
return writer;
},

decode(input: _m0.Reader | Uint8Array, length?: number): HeroById {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHeroById();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) {
break;
}

message.id = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
};

function createBaseVillainById(): VillainById {
return { id: 0 };
}

export const VillainById = {
encode(message: VillainById, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== 0) {
writer.uint32(8).int32(message.id);
}
return writer;
},

decode(input: _m0.Reader | Uint8Array, length?: number): VillainById {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseVillainById();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) {
break;
}

message.id = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
};

function createBaseHero(): Hero {
return { id: 0, name: "" };
}

export const Hero = {
encode(message: Hero, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== 0) {
writer.uint32(8).int32(message.id);
}
if (message.name !== "") {
writer.uint32(18).string(message.name);
}
return writer;
},

decode(input: _m0.Reader | Uint8Array, length?: number): Hero {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHero();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) {
break;
}

message.id = reader.int32();
continue;
case 2:
if (tag !== 18) {
break;
}

message.name = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
};

function createBaseVillain(): Villain {
return { id: 0, name: "" };
}

export const Villain = {
encode(message: Villain, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== 0) {
writer.uint32(8).int32(message.id);
}
if (message.name !== "") {
writer.uint32(18).string(message.name);
}
return writer;
},

decode(input: _m0.Reader | Uint8Array, length?: number): Villain {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseVillain();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) {
break;
}

message.id = reader.int32();
continue;
case 2:
if (tag !== 18) {
break;
}

message.name = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
};

export interface HeroServiceClient {
findOneHero(request: HeroById, metadata?: Metadata): Observable<Hero>;

Expand Down Expand Up @@ -57,3 +223,40 @@ export function HeroServiceControllerMethods() {
}

export const HERO_SERVICE_NAME = "HeroService";

export type HeroServiceService = typeof HeroServiceService;
export const HeroServiceService = {
findOneHero: {
path: "/hero.HeroService/FindOneHero",
requestStream: false,
responseStream: false,
requestSerialize: (value: HeroById) => Buffer.from(HeroById.encode(value).finish()),
requestDeserialize: (value: Buffer) => HeroById.decode(value),
responseSerialize: (value: Hero) => Buffer.from(Hero.encode(value).finish()),
responseDeserialize: (value: Buffer) => Hero.decode(value),
},
findOneVillain: {
path: "/hero.HeroService/FindOneVillain",
requestStream: false,
responseStream: false,
requestSerialize: (value: VillainById) => Buffer.from(VillainById.encode(value).finish()),
requestDeserialize: (value: Buffer) => VillainById.decode(value),
responseSerialize: (value: Villain) => Buffer.from(Villain.encode(value).finish()),
responseDeserialize: (value: Buffer) => Villain.decode(value),
},
findManyVillain: {
path: "/hero.HeroService/FindManyVillain",
requestStream: true,
responseStream: true,
requestSerialize: (value: VillainById) => Buffer.from(VillainById.encode(value).finish()),
requestDeserialize: (value: Buffer) => VillainById.decode(value),
responseSerialize: (value: Villain) => Buffer.from(Villain.encode(value).finish()),
responseDeserialize: (value: Buffer) => Villain.decode(value),
},
} as const;

export interface HeroServiceServer extends UntypedServiceImplementation {
findOneHero: handleUnaryCall<HeroById, Hero>;
findOneVillain: handleUnaryCall<VillainById, Villain>;
findManyVillain: handleBidiStreamingCall<VillainById, Villain>;
}
66 changes: 32 additions & 34 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,52 +310,50 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri

visitServices(fileDesc, sourceInfo, (serviceDesc, sInfo) => {
if (options.nestJs) {
// NestJS is sufficiently different that we special case all of the client/server interfaces

// NestJS is sufficiently different that we special case the client/server interfaces
// generate nestjs grpc client interface
chunks.push(generateNestjsServiceClient(ctx, fileDesc, sInfo, serviceDesc));
// and the service controller interface
chunks.push(generateNestjsServiceController(ctx, fileDesc, sInfo, serviceDesc));
// generate nestjs grpc service controller decorator
chunks.push(generateNestjsGrpcServiceMethodsDecorator(ctx, serviceDesc));

let serviceConstName = `${camelToSnake(serviceDesc.name)}_NAME`;
if (!serviceDesc.name.toLowerCase().endsWith("service")) {
serviceConstName = `${camelToSnake(serviceDesc.name)}_SERVICE_NAME`;
}

chunks.push(code`export const ${serviceConstName} = "${serviceDesc.name}";`);
} else {
const uniqueServices = [...new Set(options.outputServices)].sort();
uniqueServices.forEach((outputService) => {
if (outputService === ServiceOption.GRPC) {
chunks.push(generateGrpcJsService(ctx, fileDesc, sInfo, serviceDesc));
} else if (outputService === ServiceOption.NICE_GRPC) {
chunks.push(generateNiceGrpcService(ctx, fileDesc, sInfo, serviceDesc));
} else if (outputService === ServiceOption.GENERIC) {
chunks.push(generateGenericServiceDefinition(ctx, fileDesc, sInfo, serviceDesc));
} else if (outputService === ServiceOption.DEFAULT) {
// This service could be Twirp or grpc-web or JSON (maybe). So far all of their
// interfaces are fairly similar so we share the same service interface.
chunks.push(generateService(ctx, fileDesc, sInfo, serviceDesc));

if (options.outputClientImpl === true) {
chunks.push(generateServiceClientImpl(ctx, fileDesc, serviceDesc));
} else if (options.outputClientImpl === "grpc-web") {
chunks.push(generateGrpcClientImpl(ctx, fileDesc, serviceDesc));
chunks.push(generateGrpcServiceDesc(fileDesc, serviceDesc));
serviceDesc.method.forEach((method) => {
if (!method.clientStreaming) {
chunks.push(generateGrpcMethodDesc(ctx, serviceDesc, method));
}
if (method.serverStreaming) {
hasServerStreamingMethods = true;
}
});
}
}
});
}

const uniqueServices = [...new Set(options.outputServices)].sort();
uniqueServices.forEach((outputService) => {
if (outputService === ServiceOption.GRPC) {
chunks.push(generateGrpcJsService(ctx, fileDesc, sInfo, serviceDesc));
} else if (outputService === ServiceOption.NICE_GRPC) {
chunks.push(generateNiceGrpcService(ctx, fileDesc, sInfo, serviceDesc));
} else if (outputService === ServiceOption.GENERIC) {
chunks.push(generateGenericServiceDefinition(ctx, fileDesc, sInfo, serviceDesc));
} else if (outputService === ServiceOption.DEFAULT) {
// This service could be Twirp or grpc-web or JSON (maybe). So far all of their
// interfaces are fairly similar so we share the same service interface.
chunks.push(generateService(ctx, fileDesc, sInfo, serviceDesc));

if (options.outputClientImpl === true) {
chunks.push(generateServiceClientImpl(ctx, fileDesc, serviceDesc));
} else if (options.outputClientImpl === "grpc-web") {
chunks.push(generateGrpcClientImpl(ctx, fileDesc, serviceDesc));
chunks.push(generateGrpcServiceDesc(fileDesc, serviceDesc));
serviceDesc.method.forEach((method) => {
if (!method.clientStreaming) {
chunks.push(generateGrpcMethodDesc(ctx, serviceDesc, method));
}
if (method.serverStreaming) {
hasServerStreamingMethods = true;
}
});
}
}
});

serviceDesc.method.forEach((methodDesc, _index) => {
if (methodDesc.serverStreaming || methodDesc.clientStreaming) {
hasStreamingMethods = true;
Expand Down
12 changes: 7 additions & 5 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { parse } from "path";
import { Code } from "ts-poet";
import { ToStringOpts } from "ts-poet/build/Code";

export enum LongOption {
Expand Down Expand Up @@ -202,15 +200,19 @@ export function optionsFromParameter(parameter: string | undefined): Options {
if ((options.outputServices as any) === false) {
options.outputServices = [ServiceOption.NONE];
}

// Existing type-coercion inside parseParameter leaves a little to be desired.
if (typeof options.outputServices == "string") {
options.outputServices = [options.outputServices];
}

if (options.outputServices.length == 0) {
// Assume the user wants the default service output, unless they're using nestJs, which has
// its own controllers output (although nestjs users can ask for other services too).
if (options.outputServices.length == 0 && !options.nestJs) {
options.outputServices = [ServiceOption.DEFAULT];
}
// If using nestJs + other services, add the encode methods back
if (options.nestJs && options.outputServices.length > 0) {
options.outputEncodeMethods = true;
}

if ((options.useDate as any) === true) {
// Treat useDate=true as DATE
Expand Down
2 changes: 0 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,9 @@ export class FormattedMethodDescriptor implements MethodDescriptorProto {
*/
public static formatName(methodName: string, options: Options) {
let result = methodName;

if (options.lowerCaseServiceMethods || options.outputServices.includes(ServiceOption.GRPC)) {
if (options.snakeToCamel) result = camelCaseGrpc(result);
}

return result;
}
}
Expand Down
8 changes: 3 additions & 5 deletions tests/options-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { DateOption, optionsFromParameter, ServiceOption } from "../src/options"

describe("options", () => {
it("can set outputJsonMethods with nestJs=true", () => {
console.log(optionsFromParameter("nestJs=true,outputJsonMethods=true"));
expect(optionsFromParameter("nestJs=true,outputJsonMethods=true")).toMatchInlineSnapshot(`
{
"M": {},
Expand Down Expand Up @@ -33,9 +32,7 @@ describe("options", () => {
"outputJsonMethods": true,
"outputPartialMethods": false,
"outputSchema": false,
"outputServices": [
"default",
],
"outputServices": [],
"outputTypeAnnotations": false,
"outputTypeRegistry": false,
"removeEnumPrefix": false,
Expand Down Expand Up @@ -70,11 +67,12 @@ describe("options", () => {
`);
});

it("can set outputJsonMethods with nestJs=true", () => {
it("can set addGrpcMetadata=false", () => {
const options = optionsFromParameter("outputClientImpl=grpc-web,addGrpcMetadata=false");
expect(options).toMatchObject({
outputClientImpl: "grpc-web",
addGrpcMetadata: false,
outputServices: ["default"],
});
});

Expand Down

0 comments on commit 67f3019

Please sign in to comment.