ts-proto
Version:
> `ts-proto` transforms your `.proto` files into strongly-typed, idiomatic TypeScript files!
211 lines (210 loc) • 11.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateDataLoadersType = exports.generateRpcType = exports.generateServiceClientImpl = exports.generateService = void 0;
const types_1 = require("./types");
const ts_poet_1 = require("ts-poet");
const utils_1 = require("./utils");
const sourceInfo_1 = require("./sourceInfo");
const case_1 = require("./case");
const main_1 = require("./main");
const dataloader = ts_poet_1.TypeNames.anyType('DataLoader*dataloader');
/**
* Generates an interface for `serviceDesc`.
*
* Some RPC frameworks (i.e. Twirp) can use the same interface, i.e.
* `getFoo(req): Promise<res>` for the client-side and server-side,
* which is the intent for this interface.
*
* Other RPC frameworks (i.e. NestJS) that need different client-side
* vs. server-side code/interfaces are handled separately.
*/
function generateService(typeMap, fileDesc, sourceInfo, serviceDesc, options) {
let service = ts_poet_1.InterfaceSpec.create(serviceDesc.name).addModifiers(ts_poet_1.Modifier.EXPORT);
if (options.useContext) {
service = service.addTypeVariable(main_1.contextTypeVar);
}
utils_1.maybeAddComment(sourceInfo, (text) => (service = service.addJavadoc(text)));
serviceDesc.method.forEach((methodDesc, index) => {
if (options.lowerCaseServiceMethods) {
methodDesc.name = case_1.camelCase(methodDesc.name);
}
let requestFn = ts_poet_1.FunctionSpec.create(methodDesc.name);
if (options.useContext) {
requestFn = requestFn.addParameter('ctx', ts_poet_1.TypeNames.typeVariable('Context'));
}
const info = sourceInfo.lookup(sourceInfo_1.Fields.service.method, index);
utils_1.maybeAddComment(info, (text) => (requestFn = requestFn.addJavadoc(text)));
let inputType = types_1.requestType(typeMap, methodDesc, options);
// the grpc-web clients `fromPartial` the input before handing off to grpc-web's
// serde runtime, so it's okay to accept partial results from the client
if (options.outputClientImpl === 'grpc-web') {
inputType = ts_poet_1.TypeNames.parameterizedType(ts_poet_1.TypeNames.anyType('DeepPartial'), inputType);
}
requestFn = requestFn.addParameter('request', inputType);
// Use metadata as last argument for interface only configuration
if (options.outputClientImpl === 'grpc-web') {
requestFn = requestFn.addParameter('metadata?', 'grpc.Metadata');
}
else if (options.addGrpcMetadata) {
requestFn = requestFn.addParameter(options.addNestjsRestParameter ? 'metadata' : 'metadata?', 'Metadata@grpc');
}
if (options.addNestjsRestParameter) {
requestFn = requestFn.addParameter('...rest', 'any');
}
// Return observable for interface only configuration, passing returnObservable=true and methodDesc.serverStreaming=true
if (options.returnObservable || methodDesc.serverStreaming) {
requestFn = requestFn.returns(types_1.responseObservable(typeMap, methodDesc, options));
}
else {
requestFn = requestFn.returns(types_1.responsePromise(typeMap, methodDesc, options));
}
service = service.addFunction(requestFn);
if (options.useContext) {
const batchMethod = types_1.detectBatchMethod(typeMap, fileDesc, serviceDesc, methodDesc, options);
if (batchMethod) {
const name = batchMethod.methodDesc.name.replace('Batch', 'Get');
let batchFn = ts_poet_1.FunctionSpec.create(name);
if (options.useContext) {
batchFn = batchFn.addParameter('ctx', ts_poet_1.TypeNames.typeVariable('Context'));
}
batchFn = batchFn.addParameter(utils_1.singular(batchMethod.inputFieldName), batchMethod.inputType);
batchFn = batchFn.returns(ts_poet_1.TypeNames.PROMISE.param(batchMethod.outputType));
service = service.addFunction(batchFn);
}
}
});
return service;
}
exports.generateService = generateService;
function generateRegularRpcMethod(options, typeMap, fileDesc, serviceDesc, methodDesc) {
let requestFn = ts_poet_1.FunctionSpec.create(methodDesc.name);
if (options.useContext) {
requestFn = requestFn.addParameter('ctx', ts_poet_1.TypeNames.typeVariable('Context'));
}
let inputType = types_1.requestType(typeMap, methodDesc, options);
return requestFn
.addParameter('request', inputType)
.addStatement('const data = %L.encode(request).finish()', inputType)
.addStatement('const promise = this.rpc.request(%L"%L.%L", %S, %L)', options.useContext ? 'ctx, ' : '', // sneak ctx in as the 1st parameter to our rpc call
fileDesc.package, serviceDesc.name, methodDesc.name, 'data')
.addStatement('return promise.then(data => %L.decode(new %T(data)))', types_1.responseType(typeMap, methodDesc, options), 'Reader@protobufjs/minimal')
.returns(types_1.responsePromise(typeMap, methodDesc, options));
}
function generateServiceClientImpl(typeMap, fileDesc, serviceDesc, options) {
// Define the FooServiceImpl class
let client = ts_poet_1.ClassSpec.create(`${serviceDesc.name}ClientImpl`).addModifiers(ts_poet_1.Modifier.EXPORT);
if (options.useContext) {
client = client.addTypeVariable(main_1.contextTypeVar);
client = client.addInterface(`${serviceDesc.name}<Context>`);
}
else {
client = client.addInterface(serviceDesc.name);
}
// Create the constructor(rpc: Rpc)
const rpcType = options.useContext ? 'Rpc<Context>' : 'Rpc';
client = client.addFunction(ts_poet_1.FunctionSpec.createConstructor().addParameter('rpc', rpcType).addStatement('this.rpc = rpc'));
client = client.addProperty('rpc', rpcType, { modifiers: [ts_poet_1.Modifier.PRIVATE, ts_poet_1.Modifier.READONLY] });
// Create a method for each FooService method
for (const methodDesc of serviceDesc.method) {
// See if this this fuzzy matches to a batchable method
if (options.useContext) {
const batchMethod = types_1.detectBatchMethod(typeMap, fileDesc, serviceDesc, methodDesc, options);
if (batchMethod) {
client = client.addFunction(generateBatchingRpcMethod(typeMap, batchMethod));
}
}
if (options.useContext && methodDesc.name.match(/^Get[A-Z]/)) {
client = client.addFunction(generateCachingRpcMethod(options, typeMap, fileDesc, serviceDesc, methodDesc));
}
else {
client = client.addFunction(generateRegularRpcMethod(options, typeMap, fileDesc, serviceDesc, methodDesc));
}
}
return client;
}
exports.generateServiceClientImpl = generateServiceClientImpl;
/** We've found a BatchXxx method, create a synthetic GetXxx method that calls it. */
function generateBatchingRpcMethod(typeMap, batchMethod) {
const { methodDesc, singleMethodName, inputFieldName, inputType, outputFieldName, outputType, mapType, uniqueIdentifier, } = batchMethod;
// Create the `(keys) => ...` lambda we'll pass to the DataLoader constructor
let lambda = ts_poet_1.CodeBlock.lambda(inputFieldName) // e.g. keys
.addStatement('const request = { %L }', inputFieldName);
if (mapType) {
// If the return type is a map, lookup each key in the result
lambda = lambda
.beginLambda('return this.%L(ctx, request).then(res =>', methodDesc.name)
.addStatement('return %L.map(key => res.%L[key])', inputFieldName, outputFieldName)
.endLambda(')');
}
else {
// Otherwise assume they come back in order
lambda = lambda.addStatement('return this.%L(ctx, request).then(res => res.%L)', methodDesc.name, outputFieldName);
}
return ts_poet_1.FunctionSpec.create(singleMethodName)
.addParameter('ctx', 'Context')
.addParameter(utils_1.singular(inputFieldName), inputType)
.addCode('const dl = ctx.getDataLoader(%S, () => {%>\n', uniqueIdentifier)
.addCode('return new %T<%T, %T>(%L, { cacheKeyFn: %T });\n', dataloader, inputType, outputType, lambda, ts_poet_1.TypeNames.anyType('hash*object-hash'))
.addCode('%<});\n')
.addStatement('return dl.load(%L)', utils_1.singular(inputFieldName))
.returns(ts_poet_1.TypeNames.PROMISE.param(outputType));
}
/** We're not going to batch, but use DataLoader for per-request caching. */
function generateCachingRpcMethod(options, typeMap, fileDesc, serviceDesc, methodDesc) {
const inputType = types_1.requestType(typeMap, methodDesc, options);
const outputType = types_1.responseType(typeMap, methodDesc, options);
let lambda = ts_poet_1.CodeBlock.lambda('requests')
.beginLambda('const responses = requests.map(async request =>')
.addStatement('const data = %L.encode(request).finish()', inputType)
.addStatement('const response = await this.rpc.request(ctx, "%L.%L", %S, %L)', fileDesc.package, serviceDesc.name, methodDesc.name, 'data')
.addStatement('return %L.decode(new %T(response))', outputType, 'Reader@protobufjs/minimal')
.endLambda(')')
.addStatement('return Promise.all(responses)');
const uniqueIdentifier = `${fileDesc.package}.${serviceDesc.name}.${methodDesc.name}`;
return ts_poet_1.FunctionSpec.create(methodDesc.name)
.addParameter('ctx', 'Context')
.addParameter('request', inputType)
.addCode('const dl = ctx.getDataLoader(%S, () => {%>\n', uniqueIdentifier)
.addCode('return new %T<%T, %T>(%L, { cacheKeyFn: %T });\n', dataloader, inputType, outputType, lambda, ts_poet_1.TypeNames.anyType('hash*object-hash'))
.addCode('%<});\n')
.addStatement('return dl.load(request)')
.returns(ts_poet_1.TypeNames.PROMISE.param(outputType));
}
/**
* Creates an `Rpc.request(service, method, data)` abstraction.
*
* This lets clients pass in their own request-promise-ish client.
*
* We don't export this because if a project uses multiple `*.proto` files,
* we don't want our the barrel imports in `index.ts` to have multiple `Rpc`
* types.
*/
function generateRpcType(options) {
const data = ts_poet_1.TypeNames.anyType('Uint8Array');
let fn = ts_poet_1.FunctionSpec.create('request');
if (options.useContext) {
fn = fn.addParameter('ctx', 'Context');
}
fn = fn
.addParameter('service', ts_poet_1.TypeNames.STRING)
.addParameter('method', ts_poet_1.TypeNames.STRING)
.addParameter('data', data)
.returns(ts_poet_1.TypeNames.PROMISE.param(data));
let rpc = ts_poet_1.InterfaceSpec.create('Rpc');
if (options.useContext) {
rpc = rpc.addTypeVariable(ts_poet_1.TypeNames.typeVariable('Context'));
}
rpc = rpc.addFunction(fn);
return rpc;
}
exports.generateRpcType = generateRpcType;
function generateDataLoadersType() {
// TODO Maybe should be a generic `Context.get<T>(id, () => T): T` method
let fn = ts_poet_1.FunctionSpec.create('getDataLoader')
.addTypeVariable(ts_poet_1.TypeNames.typeVariable('T'))
.addParameter('identifier', ts_poet_1.TypeNames.STRING)
.addParameter('constructorFn', ts_poet_1.TypeNames.lambda2([], ts_poet_1.TypeNames.typeVariable('T')))
.returns(ts_poet_1.TypeNames.typeVariable('T'));
return ts_poet_1.InterfaceSpec.create('DataLoaders').addModifiers(ts_poet_1.Modifier.EXPORT).addFunction(fn);
}
exports.generateDataLoadersType = generateDataLoadersType;