1 | # Thrift Client
|
2 |
|
3 | Thrift client library for NodeJS written in TypeScript.
|
4 |
|
5 | Supports communicating with Thrift services over HTTP or TCP.
|
6 |
|
7 | ## Usage
|
8 |
|
9 | We're going to go through this step-by-step.
|
10 |
|
11 | * Install dependencies
|
12 | * Define our service
|
13 | * Run codegen on our Thrift IDL
|
14 | * Create a client
|
15 | * Make service calls with our client
|
16 |
|
17 | ### Install
|
18 |
|
19 | All Thrift Server libraries define inter-dependencies as peer dependencies to avoid type collisions.
|
20 |
|
21 | ```sh
|
22 | $ npm install --save-dev @creditkarma/thrift-typescript
|
23 | $ npm install --save @creditkarma/thrift-server-core
|
24 | $ npm install --save @creditkarma/thrift-client
|
25 | $ npm install --save request
|
26 | $ npm install --save @types/request
|
27 | ```
|
28 |
|
29 | ### Example Service
|
30 |
|
31 | ```c
|
32 | service Calculator {
|
33 | i32 add(1: i32 left, 2: i32 right)
|
34 | i32 subtract(1: i32 left, 2: i32 right)
|
35 | }
|
36 | ```
|
37 |
|
38 | ### Codegen
|
39 |
|
40 | Requires @creditkarma/thrift-typescript >= v3.0.0
|
41 |
|
42 | Add a script to your package.json to codegen. The 'target' option is important to make thrift-typescript generate for this library instead of the Apache libraries.
|
43 |
|
44 | ```json
|
45 | "scripts": {
|
46 | "codegen": "thrift-typescript --target thrift-server --sourceDir thrift --outDir codegen"
|
47 | }
|
48 | ```
|
49 |
|
50 | Then you can run codegen:
|
51 |
|
52 | ```sh
|
53 | $ npm run codegen
|
54 | ```
|
55 |
|
56 | ### Creating an HTTP Client
|
57 |
|
58 | There are two ways to create HTTP clients with the public API.
|
59 |
|
60 | * Use the `createHttpClient` factory function.
|
61 | * Manually create your own `HttpConnection` object
|
62 |
|
63 | #### `createHttpClient`
|
64 |
|
65 | Using the `createHttpClient` function you pass in two arguments, the first is your Thrift service client class and the second is a map of options to configure the underlying HTTP connection.
|
66 |
|
67 | When creating a client using this method Thrift Client uses the [Request library](https://github.com/request/request) for making HTTP requests.
|
68 |
|
69 | ```typescript
|
70 | import {
|
71 | createHttpClient
|
72 | } from '@creditkaram/thrift-client'
|
73 |
|
74 | import { CoreOptions } from 'request'
|
75 |
|
76 | import { Calculator } from './codegen/calculator'
|
77 |
|
78 | // Create Thrift client
|
79 | const thriftClient: Calculator.Client<CoreOptions> = createHttpClient(Calculator.Client, {
|
80 | serviceName: 'calculator-service',
|
81 | hostName: 'localhost',
|
82 | port: 8080,
|
83 | requestOptions: {} // CoreOptions to pass to Request
|
84 | })
|
85 | ```
|
86 |
|
87 | ##### Options
|
88 |
|
89 | The available options are:
|
90 |
|
91 | * serviceName (optional): The name of your service. Used for logging.
|
92 | * hostName (required): The name of the host to connect to.
|
93 | * port (required): The port number to attach to on the host.
|
94 | * path (optional): The path on which the Thrift service is listening. Defaults to '/thrift'.
|
95 | * https (optional): Boolean value indicating whether to use https. Defaults to false.
|
96 | * transport (optional): Name of the Thrift transport type to use. Defaults to 'buffered'.
|
97 | * protocol (optional): Name of the Thrift protocol type to use. Defaults to 'binary'.
|
98 | * requestOptions (optional): Options to pass to the underlying [Request library](https://github.com/request/request#requestoptions-callback). Defaults to {}.
|
99 | * register (optional): A list of filters to apply to your client. More on this later.
|
100 |
|
101 | Currently `@creditkarma/thrift-server-core"` only supports buffered transport and binary or compact protocols.
|
102 |
|
103 | ```typescript
|
104 | type TransportType = 'buffered'
|
105 | ```
|
106 |
|
107 | The possible protocol types are:
|
108 |
|
109 | ```typescript
|
110 | type ProtocolType = 'binary' | 'compact'
|
111 | ```
|
112 |
|
113 | #### Manual Creation
|
114 |
|
115 | Manually creating your Thrift client allows you to choose the use of another HTTP client library or to reuse a previously created instance of Request.
|
116 |
|
117 | ```typescript
|
118 | import {
|
119 | RequestInstance,
|
120 | HttpConnection,
|
121 | IHttpConnectionOptions,
|
122 | } from '@creditkaram/thrift-client'
|
123 | import * as request from 'request'
|
124 | import { CoreOptions } from 'request'
|
125 |
|
126 | import { Calculator } from './codegen/calculator'
|
127 |
|
128 | const clientConfig: IHttpConnectionOptions = {
|
129 | hostName: 'localhost',
|
130 | port: 3000,
|
131 | path: '/',
|
132 | transport: 'buffered',
|
133 | protocol: 'binary',
|
134 | }
|
135 |
|
136 | // Create Thrift client
|
137 | const requestClient: RequestInstance = request.defaults({})
|
138 |
|
139 | const connection: HttpConnection =
|
140 | new HttpConnection(requestClient, clientConfig)
|
141 |
|
142 | const thriftClient: Calculator.Client<CoreOptions> = new Calculator.Client(connection)
|
143 | ```
|
144 |
|
145 | Here `HttpConnection` is a class that extends the `ThriftConnection` abstract class. You could create custom connections, for instance TCP, by extending the same class.
|
146 |
|
147 | Also of note here is that the type `IHttpConnectionOptions` does not accept the `requestOptions` parameter. Options to `Request` here would be passed directly to the call to `request.defaults({})`.
|
148 |
|
149 | ### Creating a TCP Client
|
150 |
|
151 | Creating a TCP client is much like creating an HTTP client.
|
152 |
|
153 | * Use the `createTcpClient` factory function.
|
154 | * Manually create your own `TcpConnection` object
|
155 |
|
156 | *Note: Our TcpConnection object uses an underlying connection pool instead of all client requests reusing the same connection. This pool is configurable. We use [GenericPool](https://github.com/coopernurse/node-pool) for managing our connection pool. Pool options are passed directly through to GenericPool.*
|
157 |
|
158 | #### `createTcpClient`
|
159 |
|
160 | Using the `createTcpClient` function you pass in two arguments, the first is your Thrift service client class and the second is a map of options to configure the underlying TCP connection.
|
161 |
|
162 | ```typescript
|
163 | import {
|
164 | createTcpClient
|
165 | } from '@creditkaram/thrift-client'
|
166 |
|
167 | import { Calculator } from './codegen/calculator'
|
168 |
|
169 | // Create Thrift client
|
170 | const thriftClient: Calculator.Client<CoreOptions> = createTcpClient(Calculator.Client, {
|
171 | serviceName: 'calculator-service',
|
172 | hostName: 'localhost',
|
173 | port: 8080,
|
174 | })
|
175 | ```
|
176 |
|
177 | ##### Options
|
178 |
|
179 | The available options are:
|
180 |
|
181 | * serviceName (optional): The name of your service. Used for logging.
|
182 | * hostName (required): The name of the host to connect to.
|
183 | * port (required): The port number to attach to on the host.
|
184 | * timeout (optional): Connection timeout in milliseconds. Defaults to 5000.
|
185 | * transport (optional): Name of the Thrift transport type to use. Defaults to 'buffered'.
|
186 | * protocol (optional): Name of the Thrift protocol type to use. Defaults to 'binary'.
|
187 | * pool (optional): Options to configure the underlying connection pool.
|
188 | * register (optional): A list of filters to apply to your client. More on this later.
|
189 |
|
190 | #### Manual Creation
|
191 |
|
192 | ```typescript
|
193 | import {
|
194 | TcpConnection,
|
195 | } from '@creditkaram/thrift-client'
|
196 |
|
197 | import { Calculator } from './codegen/calculator'
|
198 |
|
199 | const connection: TcpConnection = new TcpConnection({
|
200 | hostName: 'localhost',
|
201 | port: 3000,
|
202 | transport: 'buffered',
|
203 | protocol: 'binary',
|
204 | })
|
205 |
|
206 | const thriftClient: Calculator.Client<CoreOptions> = new Calculator.Client(connection)
|
207 | ```
|
208 |
|
209 | Here `TcpConnection` is a class that extends the `ThriftConnection` abstract class. You could create custom connections, for instance TCP, by extending the same class.
|
210 |
|
211 | ### Making Service Calls with our Client
|
212 |
|
213 | However we chose to make our client, we use them in the same way.
|
214 |
|
215 | Notice the optional context parameter. All service client methods can take an optional context parameter. This context refers to the request options for Request library (CoreOptions). These options will be deep merged with any default options (passed in on instantiation) before sending a service request. This context can be used to do useful things like tracing or authentication. Usually this will be used for changing headers on a per-request basis.
|
216 |
|
217 | Related to context you will notice that our Thrift service client is a generic `Calculator.Client<ThriftContext<CoreOptions>>`. This type parameter refers to the type of the context, here the `ThriftContext<CoreOptions>` which extends the options interface from the Request library.
|
218 |
|
219 | ```typescript
|
220 | import {
|
221 | createHttpClient
|
222 | } from '@creditkaram/thrift-client'
|
223 |
|
224 | import { CoreOptions } from 'request'
|
225 | import * as express from 'express'
|
226 |
|
227 | import { Calculator } from './codegen/calculator'
|
228 |
|
229 | const serverConfig = {
|
230 | hostName: 'localhost',
|
231 | port: 8080,
|
232 | }
|
233 |
|
234 | // Create Thrift client
|
235 | const thriftClient: Calculator.Client<ThriftContext<CoreOptions>> = createHttpClient(Calculator.Client, {
|
236 | hostName: 'localhost',
|
237 | port: 8080,
|
238 | requestOptions: {} // CoreOptions to pass to Request
|
239 | })
|
240 |
|
241 | // This receives a query like "http://localhost:8080/add?left=5&right=3"
|
242 | app.get('/add', (req: express.Request, res: express.Response): void => {
|
243 | // Request contexts allow you to do tracing and auth
|
244 | const context: CoreOptions = {
|
245 | headers: {
|
246 | 'x-trace-id': 'my-trace-id'
|
247 | }
|
248 | }
|
249 |
|
250 | // Client methods return a Promise of the expected result
|
251 | thriftClient.add(req.query.left, req.query.right, context).then((result: number) => {
|
252 | res.send(result)
|
253 | }, (err: any) => {
|
254 | res.status(500).send(err)
|
255 | })
|
256 | })
|
257 |
|
258 | app.listen(serverConfig.port, () => {
|
259 | console.log(`Web server listening at http://${serverConfig.hostName}:${serverConfig.port}`)
|
260 | })
|
261 | ```
|
262 |
|
263 | ### Client Filters
|
264 |
|
265 | Sometimes you'll want to universally filter or modify requests and/or responses. This is done with filters. If you've used many server libraries you are probably used to the idea of plugins or middleware.
|
266 |
|
267 | A filter is an object that consists of a handler function and an optional list of client method names to apply the filter to.
|
268 |
|
269 | Filters are applied in the order in which they are registered.
|
270 |
|
271 | ```typescript
|
272 | interface IRequestResponse {
|
273 | statusCode: number
|
274 | headers: IRequestHeaders
|
275 | body: Buffer
|
276 | }
|
277 |
|
278 | type NextFunction<Options> =
|
279 | (data?: Buffer, options?: Options) => Promise<IRequestResponse>
|
280 |
|
281 | interface IThriftClientFilterConfig<Options> {
|
282 | methods?: Array<string>
|
283 | handler(request: IThriftRequest<Options>, next: NextFunction<Options>): Promise<IRequestResponse>
|
284 | }
|
285 | ```
|
286 |
|
287 | The `handler` function receives as its arguments the `IThriftRequest` object and the next `RequestHandler` in the chain. When you are done applying your filter you `return` the call to `next`. When calling `next` you can optionally pass along a modified Thrift data `Buffer` or new options to apply to the request. `Options` is almost always going to be `CoreOptions` for the underlying request library.
|
288 |
|
289 | #### IThriftRequest
|
290 |
|
291 | The `IThriftRequest` object wraps the outgoing data with some useful metadata about the current request.
|
292 |
|
293 | ```typescript
|
294 | export interface IThriftRequest<Context> {
|
295 | data: Buffer
|
296 | methodName: string
|
297 | uri: string
|
298 | context: Context
|
299 | }
|
300 | ```
|
301 |
|
302 | The `data` attribute is the outgoing Thrift payload. The `methodName` is the name of the Thrift service method being called.
|
303 |
|
304 | The other interesting one is `context`. The context is the data passed through in the service method call.
|
305 |
|
306 | If we look back at our previous client example we find this:
|
307 |
|
308 | ```typescript
|
309 | // This receives a query like "http://localhost:8080/add?left=5&right=3"
|
310 | app.get('/add', (req: express.Request, res: express.Response): void => {
|
311 | // Request contexts allow you to do tracing and auth
|
312 | const context: CoreOptions = {
|
313 | headers: {
|
314 | 'x-trace-id': 'my-trace-id'
|
315 | }
|
316 | }
|
317 |
|
318 | // Client methods return a Promise of the expected result
|
319 | thriftClient.add(req.query.left, req.query.right, context).then((result: number) => {
|
320 | res.send(result)
|
321 | }, (err: any) => {
|
322 | res.status(500).send(err)
|
323 | })
|
324 | })
|
325 | ```
|
326 |
|
327 | Where the context passed through is an instance of `CoreOptions`. These options are passed through to modify the outgoing request. Any filter in the filter chain can modify this data.
|
328 |
|
329 | To modify this context one needs to pass the updated context to the `next` function.
|
330 |
|
331 | ```typescript
|
332 | return next(request.data, updatedContext)
|
333 | ```
|
334 |
|
335 | #### Applying Filters to Outgoing Requests
|
336 |
|
337 | Something you may want to do with middlware is to apply some common HTTP headers to every outgoing request from your service. Maybe there is a token your service should attach to every outgoing request.
|
338 |
|
339 | You could do something like this:
|
340 |
|
341 | ```typescript
|
342 | import {
|
343 | createHttpClient,
|
344 | IThriftRequest,
|
345 | } from '@creditkaram/thrift-client'
|
346 |
|
347 | import { Calculator } from './codegen/calculator'
|
348 |
|
349 | const thriftClient: Calculator.Client = createHttpClient(Calculator.Client, {
|
350 | hostName: 'localhost',
|
351 | port: 8080,
|
352 | register: [ {
|
353 | handler(request: IThriftRequest<CoreOptions>, next: NextFunction<CoreOptions>): Promise<IRequestResponse> {
|
354 | return next(request.data, {
|
355 | headers: {
|
356 | 'x-fake-token': 'fake-token',
|
357 | },
|
358 | })
|
359 | },
|
360 | } ]
|
361 | })
|
362 | ```
|
363 |
|
364 | This sends data along unaltered, but adds a header `x-fake-token` to the outgoing request. When you send along options, the options are deep merged with any previous options that were applied.
|
365 |
|
366 | #### Applying Filters to Incoming Responses
|
367 |
|
368 | To apply filters to the response you would call `.then` on the `next` function. This would allow you to inspect or modify the response before allowing it to proceed up the chain.
|
369 |
|
370 | ```typescript
|
371 | import {
|
372 | createHttpClient,
|
373 | IThriftRequest
|
374 | } from '@creditkaram/thrift-client'
|
375 |
|
376 | import { Calculator } from './codegen/calculator'
|
377 |
|
378 | const thriftClient: Calculator.Client = createHttpClient(Calculator.Client, {
|
379 | hostName: 'localhost',
|
380 | port: 8080,
|
381 | register: [ {
|
382 | handler(request: IThriftRequest<CoreOptions>, next: NextFunction<CoreOptions>): Promise<IRequestResponse> {
|
383 | return next().then((res: IRequestResponse) => {
|
384 | if (validateResponse(res.body)) {
|
385 | return res
|
386 | } else {
|
387 | throw new Error('Invalid data returned')
|
388 | }
|
389 | })
|
390 | },
|
391 | } ]
|
392 | })
|
393 | ```
|
394 |
|
395 | #### Adding Filters to HttpConnection Object
|
396 |
|
397 | When you're not using `createHttpClient` you can add filters directly to the connection instance.
|
398 |
|
399 | ```typescript
|
400 | // Create thrift client
|
401 | const requestClient: RequestInstance = request.defaults({})
|
402 |
|
403 | const connection: HttpConnection =
|
404 | new HttpConnection(requestClient, clientConfig)
|
405 |
|
406 | connection.register({
|
407 | handler(request: IThriftRequest<CoreOptions>, next: NextFunction<CoreOptions>): Promise<IRequestResponse> {
|
408 | return next(request.data, {
|
409 | headers: {
|
410 | 'x-fake-token': 'fake-token',
|
411 | },
|
412 | })
|
413 | },
|
414 | })
|
415 |
|
416 | const thriftClient: Calculator.Client = new Calculator.Client(connection)
|
417 | ```
|
418 |
|
419 | The optional `register` option takes an array of filters to apply. Unsurprisingly they are applied in the order you pass them in.
|
420 |
|
421 | #### Available Filters
|
422 |
|
423 | These client filters are maintained as part of this repository:
|
424 |
|
425 | * [ThriftClientContextFilter](https://github.com/creditkarma/thrift-server/tree/master/packages/thrift-client-context-filter) - This is useful for TCP clients. It is a filter for appending Thrift objects onto an existing Thrift payload.
|
426 | * [ThriftClientTTwitterFilter](https://github.com/creditkarma/thrift-server/tree/master/packages/thrift-client-ttwitter-filter) - This is for appending a TTwitter context onto an existing Thrift payload. This is for compatibility with Twitter's [Finagle](https://twitter.github.io/finagle/) framework.
|
427 | * [ThriftClientZipkinFilter](https://github.com/creditkarma/thrift-server/tree/master/packages/thrift-client-zipkin-filter) - This is for supporting distributed tracing with [Zipkin](https://github.com/openzipkin/zipkin-js).
|
428 |
|
429 | ## Contributing
|
430 |
|
431 | For more information about contributing new features and bug fixes, see our [Contribution Guidelines](../../CONTRIBUTING.md).
|
432 | External contributors must sign Contributor License Agreement (CLA)
|
433 |
|
434 | ## License
|
435 |
|
436 | This project is licensed under [Apache License Version 2.0](./LICENSE)
|