UNPKG

ts-proto

Version:

[![npm](https://img.shields.io/npm/v/ts-proto)](https://www.npmjs.com/package/ts-proto) [![build](https://github.com/stephenh/ts-proto/workflows/Build/badge.svg)](https://github.com/stephenh/ts-proto/actions)

324 lines (323 loc) 15 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateDataLoaderOptionsType = exports.generateDataLoadersType = exports.generateRpcType = exports.generateServiceClientImpl = exports.generateService = void 0; const ts_poet_1 = require("ts-poet"); const types_1 = require("./types"); const utils_1 = require("./utils"); const sourceInfo_1 = require("./sourceInfo"); const main_1 = require("./main"); const hash = (0, ts_poet_1.imp)("hash*object-hash"); const dataloader = (0, ts_poet_1.imp)("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(ctx, fileDesc, sourceInfo, serviceDesc) { var _a; const { options } = ctx; const chunks = []; (0, utils_1.maybeAddComment)(sourceInfo, chunks, (_a = serviceDesc.options) === null || _a === void 0 ? void 0 : _a.deprecated); const maybeTypeVar = options.context ? `<${main_1.contextTypeVar}>` : ""; chunks.push((0, ts_poet_1.code) `export interface ${(0, ts_poet_1.def)(serviceDesc.name)}${maybeTypeVar} {`); serviceDesc.method.forEach((methodDesc, index) => { var _a; (0, utils_1.assertInstanceOf)(methodDesc, utils_1.FormattedMethodDescriptor); const info = sourceInfo.lookup(sourceInfo_1.Fields.service.method, index); (0, utils_1.maybeAddComment)(info, chunks, (_a = methodDesc.options) === null || _a === void 0 ? void 0 : _a.deprecated); const params = []; if (options.context) { params.push((0, ts_poet_1.code) `ctx: Context`); } // the grpc-web clients auto-`fromPartial` the input before handing off to grpc-web's // serde runtime, so it's okay to accept partial results from the client const partialInput = options.outputClientImpl === "grpc-web"; const inputType = (0, types_1.requestType)(ctx, methodDesc, partialInput); params.push((0, ts_poet_1.code) `request: ${inputType}`); if (options.useAbortSignal) { params.push((0, ts_poet_1.code) `abortSignal?: AbortSignal`); } // Use metadata as last argument for interface only configuration if (options.outputClientImpl === "grpc-web") { // We have to use grpc.Metadata where grpc will come from @improbable-eng params.push((0, ts_poet_1.code) `metadata?: grpc.Metadata`); } else if (options.addGrpcMetadata) { const Metadata = (0, ts_poet_1.imp)("Metadata@@grpc/grpc-js"); const q = options.addNestjsRestParameter ? "" : "?"; params.push((0, ts_poet_1.code) `metadata${q}: ${Metadata}`); } else if (options.metadataType) { const Metadata = (0, ts_poet_1.imp)(options.metadataType); params.push((0, ts_poet_1.code) `metadata?: ${Metadata}`); } if (options.addNestjsRestParameter) { params.push((0, ts_poet_1.code) `...rest: any`); } chunks.push((0, ts_poet_1.code) `${methodDesc.formattedName}(${(0, ts_poet_1.joinCode)(params, { on: "," })}): ${(0, types_1.responsePromiseOrObservable)(ctx, methodDesc)};`); // If this is a batch method, auto-generate the singular version of it if (options.context) { const batchMethod = (0, types_1.detectBatchMethod)(ctx, fileDesc, serviceDesc, methodDesc); if (batchMethod) { chunks.push((0, ts_poet_1.code) `${batchMethod.singleMethodName}( ctx: Context, ${(0, utils_1.singular)(batchMethod.inputFieldName)}: ${batchMethod.inputType}, ): Promise<${batchMethod.outputType}>;`); } } }); chunks.push((0, ts_poet_1.code) `}`); return (0, ts_poet_1.joinCode)(chunks, { on: "\n" }); } exports.generateService = generateService; function generateRegularRpcMethod(ctx, methodDesc) { (0, utils_1.assertInstanceOf)(methodDesc, utils_1.FormattedMethodDescriptor); const { options } = ctx; const Reader = (0, utils_1.impFile)(ctx.options, "Reader@protobufjs/minimal"); const rawInputType = (0, types_1.rawRequestType)(ctx, methodDesc, { keepValueType: true }); const inputType = (0, types_1.requestType)(ctx, methodDesc); const rawOutputType = (0, types_1.responseType)(ctx, methodDesc, { keepValueType: true }); const params = [ ...(options.context ? [(0, ts_poet_1.code) `ctx: Context`] : []), (0, ts_poet_1.code) `request: ${inputType}`, ...(options.useAbortSignal ? [(0, ts_poet_1.code) `abortSignal?: AbortSignal`] : []), ]; const maybeCtx = options.context ? "ctx," : ""; const maybeAbortSignal = options.useAbortSignal ? "abortSignal || undefined," : ""; let encode = (0, ts_poet_1.code) `${rawInputType}.encode(request).finish()`; let decode = (0, ts_poet_1.code) `data => ${rawOutputType}.decode(new ${Reader}(data))`; // if (options.useDate && rawOutputType.toString().includes("Timestamp")) { // decode = code`data => ${utils.fromTimestamp}(${rawOutputType}.decode(new ${Reader}(data)))`; // } if (methodDesc.clientStreaming) { if (options.useAsyncIterable) { encode = (0, ts_poet_1.code) `${rawInputType}.encodeTransform(request)`; } else { encode = (0, ts_poet_1.code) `request.pipe(${(0, ts_poet_1.imp)("map@rxjs/operators")}(request => ${encode}))`; } } let returnVariable; if (options.returnObservable || methodDesc.serverStreaming) { returnVariable = "result"; if (options.useAsyncIterable) { decode = (0, ts_poet_1.code) `${rawOutputType}.decodeTransform(result)`; } else { decode = (0, ts_poet_1.code) `result.pipe(${(0, ts_poet_1.imp)("map@rxjs/operators")}(${decode}))`; } } else { returnVariable = "promise"; decode = (0, ts_poet_1.code) `promise.then(${decode})`; } let rpcMethod; if (methodDesc.clientStreaming && methodDesc.serverStreaming) { rpcMethod = "bidirectionalStreamingRequest"; } else if (methodDesc.serverStreaming) { rpcMethod = "serverStreamingRequest"; } else if (methodDesc.clientStreaming) { rpcMethod = "clientStreamingRequest"; } else { rpcMethod = "request"; } return (0, ts_poet_1.code) ` ${methodDesc.formattedName}( ${(0, ts_poet_1.joinCode)(params, { on: "," })} ): ${(0, types_1.responsePromiseOrObservable)(ctx, methodDesc)} { const data = ${encode}; const ${returnVariable} = this.rpc.${rpcMethod}( ${maybeCtx} this.service, "${methodDesc.name}", data, ${maybeAbortSignal} ); return ${decode}; } `; } function generateServiceClientImpl(ctx, fileDesc, serviceDesc) { const { options } = ctx; const chunks = []; // Define the FooServiceImpl class const { name } = serviceDesc; const i = options.context ? `${name}<Context>` : name; const t = options.context ? `<${main_1.contextTypeVar}>` : ""; chunks.push((0, ts_poet_1.code) `export class ${name}ClientImpl${t} implements ${(0, ts_poet_1.def)(i)} {`); // Create the constructor(rpc: Rpc) const rpcType = options.context ? "Rpc<Context>" : "Rpc"; chunks.push((0, ts_poet_1.code) `private readonly rpc: ${rpcType};`); chunks.push((0, ts_poet_1.code) `private readonly service: string;`); chunks.push((0, ts_poet_1.code) `constructor(rpc: ${rpcType}, opts?: {service?: string}) {`); const serviceID = (0, utils_1.maybePrefixPackage)(fileDesc, serviceDesc.name); chunks.push((0, ts_poet_1.code) `this.service = opts?.service || "${serviceID}";`); chunks.push((0, ts_poet_1.code) `this.rpc = rpc;`); // Bind each FooService method to the FooServiceImpl class for (const methodDesc of serviceDesc.method) { (0, utils_1.assertInstanceOf)(methodDesc, utils_1.FormattedMethodDescriptor); chunks.push((0, ts_poet_1.code) `this.${methodDesc.formattedName} = this.${methodDesc.formattedName}.bind(this);`); } chunks.push((0, ts_poet_1.code) `}`); // 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.context) { const batchMethod = (0, types_1.detectBatchMethod)(ctx, fileDesc, serviceDesc, methodDesc); if (batchMethod) { chunks.push(generateBatchingRpcMethod(ctx, batchMethod)); } } if (options.context && methodDesc.name.match(/^Get[A-Z]/)) { chunks.push(generateCachingRpcMethod(ctx, fileDesc, serviceDesc, methodDesc)); } else { chunks.push(generateRegularRpcMethod(ctx, methodDesc)); } } chunks.push((0, ts_poet_1.code) `}`); return (0, ts_poet_1.code) `${chunks}`; } exports.generateServiceClientImpl = generateServiceClientImpl; /** We've found a BatchXxx method, create a synthetic GetXxx method that calls it. */ function generateBatchingRpcMethod(_ctx, batchMethod) { const { methodDesc, singleMethodName, inputFieldName, inputType, outputFieldName, outputType, mapType, uniqueIdentifier, } = batchMethod; (0, utils_1.assertInstanceOf)(methodDesc, utils_1.FormattedMethodDescriptor); // Create the `(keys) => ...` lambda we'll pass to the DataLoader constructor const lambda = []; lambda.push((0, ts_poet_1.code) ` (${inputFieldName}) => { const request = { ${inputFieldName} }; `); if (mapType) { // If the return type is a map, lookup each key in the result lambda.push((0, ts_poet_1.code) ` return this.${methodDesc.formattedName}(ctx, request).then(res => { return ${inputFieldName}.map(key => res.${outputFieldName}[key]) }); `); } else { // Otherwise assume they come back in order lambda.push((0, ts_poet_1.code) ` return this.${methodDesc.formattedName}(ctx, request).then(res => res.${outputFieldName}) `); } lambda.push((0, ts_poet_1.code) `}`); return (0, ts_poet_1.code) ` ${singleMethodName}( ctx: Context, ${(0, utils_1.singular)(inputFieldName)}: ${inputType} ): Promise<${outputType}> { const dl = ctx.getDataLoader("${uniqueIdentifier}", () => { return new ${dataloader}<${inputType}, ${outputType}>( ${(0, ts_poet_1.joinCode)(lambda)}, { cacheKeyFn: ${hash}, ...ctx.rpcDataLoaderOptions } ); }); return dl.load(${(0, utils_1.singular)(inputFieldName)}); } `; } /** We're not going to batch, but use DataLoader for per-request caching. */ function generateCachingRpcMethod(ctx, fileDesc, serviceDesc, methodDesc) { (0, utils_1.assertInstanceOf)(methodDesc, utils_1.FormattedMethodDescriptor); const inputType = (0, types_1.requestType)(ctx, methodDesc); const outputType = (0, types_1.responseType)(ctx, methodDesc); const uniqueIdentifier = `${(0, utils_1.maybePrefixPackage)(fileDesc, serviceDesc.name)}.${methodDesc.name}`; const Reader = (0, utils_1.impFile)(ctx.options, "Reader@protobufjs/minimal"); const lambda = (0, ts_poet_1.code) ` (requests) => { const responses = requests.map(async request => { const data = ${inputType}.encode(request).finish() const response = await this.rpc.request(ctx, "${(0, utils_1.maybePrefixPackage)(fileDesc, serviceDesc.name)}", "${methodDesc.name}", data); return ${outputType}.decode(new ${Reader}(response)); }); return Promise.all(responses); } `; return (0, ts_poet_1.code) ` ${methodDesc.formattedName}( ctx: Context, request: ${inputType}, ): Promise<${outputType}> { const dl = ctx.getDataLoader("${uniqueIdentifier}", () => { return new ${dataloader}<${inputType}, ${outputType}>( ${lambda}, { cacheKeyFn: ${hash}, ...ctx.rpcDataLoaderOptions }, ); }); return dl.load(request); } `; } /** * Creates an `Rpc.request(service, method, data)` abstraction. * * This lets clients pass in their own request-promise-ish client. * * This also requires clientStreamingRequest, serverStreamingRequest and * bidirectionalStreamingRequest methods if any of the RPCs is streaming. * * 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(ctx, hasStreamingMethods) { const { options } = ctx; const maybeContext = options.context ? "<Context>" : ""; const maybeContextParam = options.context ? "ctx: Context," : ""; const maybeAbortSignalParam = options.useAbortSignal ? "abortSignal?: AbortSignal," : ""; const methods = [[(0, ts_poet_1.code) `request`, (0, ts_poet_1.code) `Uint8Array`, (0, ts_poet_1.code) `Promise<Uint8Array>`]]; if (hasStreamingMethods) { const observable = (0, types_1.observableType)(ctx); methods.push([(0, ts_poet_1.code) `clientStreamingRequest`, (0, ts_poet_1.code) `${observable}<Uint8Array>`, (0, ts_poet_1.code) `Promise<Uint8Array>`]); methods.push([(0, ts_poet_1.code) `serverStreamingRequest`, (0, ts_poet_1.code) `Uint8Array`, (0, ts_poet_1.code) `${observable}<Uint8Array>`]); methods.push([ (0, ts_poet_1.code) `bidirectionalStreamingRequest`, (0, ts_poet_1.code) `${observable}<Uint8Array>`, (0, ts_poet_1.code) `${observable}<Uint8Array>`, ]); } const chunks = []; chunks.push((0, ts_poet_1.code) ` interface Rpc${maybeContext} {`); methods.forEach((method) => { chunks.push((0, ts_poet_1.code) ` ${method[0]}( ${maybeContextParam} service: string, method: string, data: ${method[1]}, ${maybeAbortSignalParam} ): ${method[2]};`); }); chunks.push((0, ts_poet_1.code) ` }`); return (0, ts_poet_1.joinCode)(chunks, { on: "\n" }); } exports.generateRpcType = generateRpcType; function generateDataLoadersType() { // TODO Maybe should be a generic `Context.get<T>(id, () => T): T` method return (0, ts_poet_1.code) ` export interface DataLoaders { rpcDataLoaderOptions?: DataLoaderOptions; getDataLoader<T>(identifier: string, constructorFn: () => T): T; } `; } exports.generateDataLoadersType = generateDataLoadersType; function generateDataLoaderOptionsType() { return (0, ts_poet_1.code) ` export interface DataLoaderOptions { cache?: boolean; } `; } exports.generateDataLoaderOptionsType = generateDataLoaderOptionsType;