1 | # fusion-plugin-rpc
|
2 |
|
3 | [![Build status](https://badge.buildkite.com/4c8b6bc04b61175d66d26b54b1d88d52e24fecb1b537c54551.svg?branch=master)](https://buildkite.com/uberopensource/fusionjs)
|
4 |
|
5 | Fetch data on the server and client with an
|
6 | [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) style interface.
|
7 |
|
8 | RPC is a natural way of expressing that a server-side function should be run in
|
9 | response to a client-side function call. Unlike
|
10 | [RESTful architectures](https://en.wikipedia.org/wiki/Representational_state_transfer),
|
11 | RPC-based architectures are not required to conform to statelessness constraints
|
12 | and are free to return session-scoped data. Additionally, the semantics of RPC
|
13 | calls are not constrained by the availability of suitably-descriptive HTTP
|
14 | methods and RPC calls can express complex state change requests more naturally
|
15 | as verbs (e.g. `returnProduct(id)`) rather than object-orientation (e.g.
|
16 | `PATCH /api/orders/:id`).
|
17 |
|
18 | If you're using React/Redux, you should use
|
19 | [`fusion-plugin-rpc-redux-react`](https://github.com/fusionjs/fusionjs/tree/master/fusion-plugin-rpc-redux-react)
|
20 | instead of this package.
|
21 |
|
22 | ---
|
23 |
|
24 | ### Table of contents
|
25 |
|
26 | - [Installation](#installation)
|
27 | - [Usage](#usage)
|
28 | - [Setup](#setup)
|
29 | - [Customization](#customization)
|
30 | - [API](#api)
|
31 | - [Registration API](#registration-api)
|
32 | - [Dependencies](#dependencies)
|
33 | - [Service API](#service-api)
|
34 | - [`mock`](#mock)
|
35 |
|
36 | ---
|
37 |
|
38 | ### Installation
|
39 |
|
40 | ```
|
41 | yarn add fusion-plugin-rpc
|
42 | ```
|
43 |
|
44 | ---
|
45 |
|
46 | ### Usage
|
47 |
|
48 | ```js
|
49 | import {createPlugin} from 'fusion-core';
|
50 | export default createPlugin({
|
51 | deps: {RPC: RPCToken},
|
52 | middleware: ({RPCFactory}) => (ctx, next) => {
|
53 | RPC.from(ctx).request('getUser', 1).then(console.log);
|
54 | }
|
55 | );
|
56 | ```
|
57 |
|
58 | ---
|
59 |
|
60 | ### Setup
|
61 |
|
62 | ```js
|
63 | // src/main.js
|
64 | import React from 'react';
|
65 | import App, {createPlugin} from 'fusion-core';
|
66 | import RPC, {
|
67 | RPCToken,
|
68 | RPCHandlersToken,
|
69 | ResponseError,
|
70 | } from 'fusion-plugin-rpc';
|
71 | import UniversalEvents, {
|
72 | UniversalEventsToken,
|
73 | } from 'fusion-plugin-universal-events';
|
74 | import {FetchToken} from 'fusion-tokens';
|
75 | import fetch from 'unfetch';
|
76 |
|
77 | // Define your rpc methods server side
|
78 | const handlers = __NODE__ && {
|
79 | getUser: async (args, ctx) => {
|
80 | return {some: 'data' + args};
|
81 | },
|
82 | test: async (args, ctx) => {
|
83 | // Error Handling Example
|
84 | try {
|
85 | doThing();
|
86 | } catch (e) {
|
87 | const error = new ResponseError('Failed to do thing');
|
88 | error.code = 'DOTHING';
|
89 | error.meta = {
|
90 | custom: 'metadata',
|
91 | };
|
92 | throw error;
|
93 | }
|
94 | },
|
95 | };
|
96 |
|
97 | export default () => {
|
98 | const app = new App(<div />);
|
99 |
|
100 | app.register(RPCToken, RPC);
|
101 | app.register(UniversalEventsToken, UniversalEvents);
|
102 | __NODE__
|
103 | ? app.register(RPCHandlersToken, handlers)
|
104 | : app.register(FetchToken, fetch);
|
105 |
|
106 | return app;
|
107 | };
|
108 | ```
|
109 |
|
110 | ---
|
111 |
|
112 | ### Customization
|
113 |
|
114 | The plugin can accept an optional config token for modifying the default behavior.
|
115 |
|
116 | #### Modify RPC Routes
|
117 |
|
118 | ```js
|
119 | // src/main.js
|
120 | import React from 'react';
|
121 | import App, {createPlugin} from 'fusion-core';
|
122 | import RPC, {
|
123 | RPCToken,
|
124 | RPCHandlersToken,
|
125 | ResponseError,
|
126 | RPCHandlersConfigToken,
|
127 | } from 'fusion-plugin-rpc';
|
128 | import UniversalEvents, {
|
129 | UniversalEventsToken,
|
130 | } from 'fusion-plugin-universal-events';
|
131 | import {FetchToken} from 'fusion-tokens';
|
132 | import fetch from 'unfetch';
|
133 |
|
134 | import handlers from './redux/handlers';
|
135 |
|
136 | export default () => {
|
137 | const app = new App(<div />);
|
138 |
|
139 | app.register(RPCHandlersConfigToken, {
|
140 | // Modify RPC endpoints to be accessible at /nested/api/rpcs/<RPC_ID>
|
141 | apiPath: 'nested/api/rpcs',
|
142 | });
|
143 |
|
144 | app.register(RPCToken, RPC);
|
145 | app.register(UniversalEventsToken, UniversalEvents);
|
146 | __NODE__
|
147 | ? app.register(RPCHandlersToken, handlers)
|
148 | : app.register(FetchToken, fetch);
|
149 |
|
150 | return app;
|
151 | };
|
152 | ```
|
153 |
|
154 | ---
|
155 |
|
156 | ### API
|
157 |
|
158 | #### Registration API
|
159 |
|
160 | ##### `RPC`
|
161 |
|
162 | ```js
|
163 | import RPC from 'fusion-plugin-rpc';
|
164 | ```
|
165 |
|
166 | The RPC plugin. Provides the RPC [service API](#service-api).
|
167 |
|
168 | ##### `RPCToken`
|
169 |
|
170 | ```js
|
171 | import {RPCToken} from 'fusion-plugin-rpc-redux-react';
|
172 | ```
|
173 |
|
174 | The canonical token for the RPC plugin. Typically, it should be registered with
|
175 | the [RPC](#rpc) plugin.
|
176 |
|
177 | #### Dependencies
|
178 |
|
179 | ##### `UniversalEventsToken`
|
180 |
|
181 | Required. See
|
182 | [https://github.com/fusionjs/fusionjs/tree/master/fusion-plugin-universal-events#api](https://github.com/fusionjs/fusionjs/tree/master/fusion-plugin-universal-events#api)
|
183 |
|
184 | ##### `RPCHandlersToken`
|
185 |
|
186 | ```js
|
187 | import {RPCHandlersToken} from 'fusion-plugin-rpc-redux-react';
|
188 | ```
|
189 |
|
190 | ##### `RPCHandlersConfigToken`
|
191 |
|
192 | ```js
|
193 | import {RPCHandlersConfigToken} from 'fusion-plugin-rpc';
|
194 | ```
|
195 |
|
196 | Configures what RPC handlers exist. Required. Server-only.
|
197 |
|
198 | ###### Types
|
199 |
|
200 | ```flow
|
201 | type RPCHandlers = Object<string, () => any>
|
202 | ```
|
203 |
|
204 | You can register a value of type `RPCHandlers` or a Plugin that provides a value
|
205 | of type `RPCHandlers`.
|
206 |
|
207 | ##### `FetchToken`
|
208 |
|
209 | Required. Browser-only. See
|
210 | [https://github.com/fusionjs/fusionjs/tree/master/fusion-tokens#fetchtoken](https://github.com/fusionjs/fusionjs/tree/master/fusion-tokens#fetchtoken)
|
211 |
|
212 | ##### `ReduxToken`
|
213 |
|
214 | Required. See
|
215 | [https://github.com/fusionjs/fusionjs/tree/master/fusion-plugin-react-redux](https://github.com/fusionjs/fusionjs/tree/master/fusion-plugin-react-redux)
|
216 |
|
217 | ##### `ReducerToken`
|
218 |
|
219 | Required. See
|
220 | [https://github.com/fusionjs/fusionjs/tree/master/fusion-plugin-react-redux](https://github.com/fusionjs/fusionjs/tree/master/fusion-plugin-react-redux)
|
221 |
|
222 | ##### `RPCHandlersConfigToken`
|
223 |
|
224 | Optional.
|
225 |
|
226 | ```flow
|
227 | type RPCConfigType = {
|
228 | apiPath?: string,
|
229 | }
|
230 | ```
|
231 |
|
232 | ---
|
233 |
|
234 | #### Service API
|
235 |
|
236 | ```js
|
237 | const rpc: RPC = Rpc.from((ctx: Context));
|
238 | ```
|
239 |
|
240 | - `ctx: Context` - Required. A
|
241 | [Fusion.js context](https://github.com/fusionjs/fusionjs/tree/master/fusion-core#context)
|
242 | - returns `rpc: {request: (method: string, args: any) => Promise<any>}`
|
243 |
|
244 | - `request: (method: string, args: any) => Promise<any>` - Makes an RPC call
|
245 | via an HTTP request. If on the server, this will directly call the `method`
|
246 | handler with `(args, ctx)`.
|
247 |
|
248 | If on the browser, this will `POST` to `/api/${method}` (unless modified;
|
249 | see [customization](#customization)) endpoint with JSON serialized args as the
|
250 | request body. The server will then deserialize the args and call the rpc
|
251 | handler. The response will be serialized and send back to the browser.
|
252 |
|
253 | - `method: string` - Required. The RPC method name
|
254 | - `args: any` - Optional. Arguments to pass to the server-side RPC handler.
|
255 | Must be JSON-serializable.
|
256 |
|
257 | ### mock
|
258 |
|
259 | The package also exports a mock RPC plugin which can be useful for testing. For
|
260 | example:
|
261 |
|
262 | ```js
|
263 | import {mock as MockRPC, RPCToken} from 'fusion-plugin-rpc';
|
264 |
|
265 | app.register(RPCToken, mock);
|
266 | ```
|
267 |
|
268 | ### Error Handling
|
269 |
|
270 | Use the `ResponseError` error subclass for sending error responses. If this
|
271 | error class is not used, a generic message will be sent to the client.
|
272 |
|
273 | ```js
|
274 | import {ResponseError} from 'fusion-plugin-rpc';
|
275 |
|
276 | function testHandler() {
|
277 | try {
|
278 | doThing();
|
279 | } catch (e) {
|
280 | const error = new ResponseError('Failed to do thing');
|
281 | error.code = 'DOTHING';
|
282 | error.meta = {
|
283 | custom: 'metadata',
|
284 | };
|
285 | throw error;
|
286 | }
|
287 | }
|
288 | ```
|
289 |
|
290 | ### Generating mock RPC handlers from fixtures
|
291 |
|
292 | The package also exports a getMockRpcHandlers util which can be useful for testing.
|
293 | Fixtures need to be of the following type
|
294 |
|
295 | ```js
|
296 | type RpcResponse = Object | ResponseError;
|
297 | type RpcResponseMap = Array<{
|
298 | args: Array<*>,
|
299 | response: RpcResponse,
|
300 | }>;
|
301 | type RpcFixtureT = {[string]: RpcResponseMap | RpcResponse};
|
302 | ```
|
303 |
|
304 | `getMockRpcHandlers` has the following interface:
|
305 |
|
306 | ```js
|
307 | type getMockRpcHandlersT = (
|
308 | fixtures: Array<RpcFixtureT>,
|
309 | onMockRpc?: OnMockRpcCallbackT
|
310 | ) => HandlerType;
|
311 | ```
|
312 |
|
313 | For example:
|
314 |
|
315 | ```js
|
316 | import {getMockRpcHandlers, ResponseError} from 'fusion-plugin-rpc';
|
317 |
|
318 | const rpcFixtures = [
|
319 | {
|
320 | getUser: {
|
321 | firstName: 'John',
|
322 | lastName: 'Doe',
|
323 | uuid: 123,
|
324 | },
|
325 | },
|
326 | {
|
327 | updateUser: [{
|
328 | args: [{firstName: 'Jane'}],
|
329 | response: {
|
330 | firstName: 'John',
|
331 | lastName: 'Doe',
|
332 | uuid: 123,
|
333 | },
|
334 | }, {
|
335 | args: [{firstName: ''}],
|
336 | response: new ResponseError('Username cant be empty'),
|
337 | }]
|
338 | },
|
339 | ];
|
340 |
|
341 | const mockRpcHandlers = getMockRpcHandlers(rpcFixtures);
|
342 |
|
343 | const user = await mockRpcHandlers.getUser();
|
344 |
|
345 | try {
|
346 | const user = await mockRpcHandlers.updateUser({firstName: ''});
|
347 | } catch (updatedUserError) {
|
348 | // When error object is passed as response in fixtures,
|
349 | // it will be considered as a failure scenario and will be thrown by rpc handler.
|
350 | }
|
351 | ```
|