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

fix: Allow other services with nestJs. #982

Merged
merged 1 commit into from
Dec 31, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ends up running even when outputServices=false.


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
Loading