1 | # fusion-plugin-rpc
|
2 |
|
3 | [![Build status](https://badge.buildkite.com/5165e82185b13861275cd0a69f29c2a13bc66dfb9461ee4af5.svg?branch=master)](https://buildkite.com/uberopensource/fusion-plugin-rpc)
|
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/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 | - [API](#api)
|
30 | - [Registration API](#registration-api)
|
31 | - [Dependencies](#dependencies)
|
32 | - [Service API](#service-api)
|
33 | - [`mock`](#mock)
|
34 |
|
35 | ---
|
36 |
|
37 | ### Installation
|
38 |
|
39 | ```
|
40 | yarn add fusion-plugin-rpc
|
41 | ```
|
42 |
|
43 | ---
|
44 |
|
45 | ### Usage
|
46 |
|
47 | ```js
|
48 | import {createPlugin} from 'fusion-core';
|
49 | export default createPlugin({
|
50 | deps: {RPC: RPCToken},
|
51 | middleware: ({RPCFactory}) => (ctx, next) => {
|
52 | RPC.from(ctx).request('getUser', 1).then(console.log);
|
53 | }
|
54 | );
|
55 | ```
|
56 |
|
57 | ---
|
58 |
|
59 | ### Setup
|
60 |
|
61 | ```js
|
62 | // src/main.js
|
63 | import React from 'react';
|
64 | import App, {createPlugin} from 'fusion-core';
|
65 | import RPC, {
|
66 | RPCToken,
|
67 | RPCHandlersToken,
|
68 | ResponseError,
|
69 | } from 'fusion-plugin-rpc';
|
70 | import UniversalEvents, {
|
71 | UniversalEventsToken,
|
72 | } from 'fusion-plugin-universal-events';
|
73 | import {FetchToken} from 'fusion-tokens';
|
74 | import fetch from 'unfetch';
|
75 |
|
76 | // Define your rpc methods server side
|
77 | const handlers = __NODE__ && {
|
78 | getUser: async (args, ctx) => {
|
79 | return {some: 'data' + args};
|
80 | },
|
81 | test: async (args, ctx) => {
|
82 | // Error Handling Example
|
83 | try {
|
84 | doThing();
|
85 | } catch (e) {
|
86 | const error = new ResponseError('Failed to do thing');
|
87 | error.code = 'DOTHING';
|
88 | error.meta = {
|
89 | custom: 'metadata',
|
90 | };
|
91 | throw error;
|
92 | }
|
93 | },
|
94 | };
|
95 |
|
96 | export default () => {
|
97 | const app = new App(<div />);
|
98 |
|
99 | app.register(RPCToken, RPC);
|
100 | app.register(UniversalEventsToken, UniversalEvents);
|
101 | __NODE__
|
102 | ? app.register(RPCHandlersToken, handlers)
|
103 | : app.register(FetchToken, fetch);
|
104 |
|
105 | return app;
|
106 | };
|
107 | ```
|
108 |
|
109 | ---
|
110 |
|
111 | ### API
|
112 |
|
113 | #### Registration API
|
114 |
|
115 | ##### `RPC`
|
116 |
|
117 | ```js
|
118 | import RPC from 'fusion-plugin-rpc';
|
119 | ```
|
120 |
|
121 | The RPC plugin. Provides the RPC [service API](#service-api).
|
122 |
|
123 | ##### `RPCToken`
|
124 |
|
125 | ```js
|
126 | import {RPCToken} from 'fusion-plugin-rpc-redux-react';
|
127 | ```
|
128 |
|
129 | The canonical token for the RPC plugin. Typically, it should be registered with
|
130 | the [RPC](#rpc) plugin.
|
131 |
|
132 | #### Dependencies
|
133 |
|
134 | ##### `UniversalEventsToken`
|
135 |
|
136 | Required. See
|
137 | [https://github.com/fusionjs/fusion-plugin-universal-events#api](https://github.com/fusionjs/fusion-plugin-universal-events#api)
|
138 |
|
139 | ##### `RPCHandlersToken`
|
140 |
|
141 | ```js
|
142 | import {RPCHandlersToken} from 'fusion-plugin-rpc-redux-react';
|
143 | ```
|
144 |
|
145 | Configures what RPC handlers exist. Required. Server-only.
|
146 |
|
147 | ###### Types
|
148 |
|
149 | ```flow
|
150 | type RPCHandlers = Object<string, () => any>
|
151 | ```
|
152 |
|
153 | You can register a value of type `RPCHandlers` or a Plugin that provides a value
|
154 | of type `RPCHandlers`.
|
155 |
|
156 | ##### `FetchToken`
|
157 |
|
158 | Required. Browser-only. See
|
159 | [https://github.com/fusionjs/fusion-tokens#fetchtoken](https://github.com/fusionjs/fusion-tokens#fetchtoken)
|
160 |
|
161 | ##### `ReduxToken`
|
162 |
|
163 | Required. See
|
164 | [https://github.com/fusionjs/fusion-plugin-react-redux](https://github.com/fusionjs/fusion-plugin-react-redux)
|
165 |
|
166 | ##### `ReducerToken`
|
167 |
|
168 | Required. See
|
169 | [https://github.com/fusionjs/fusion-plugin-react-redux](https://github.com/fusionjs/fusion-plugin-react-redux)
|
170 |
|
171 | ---
|
172 |
|
173 | #### Service API
|
174 |
|
175 | ```js
|
176 | const rpc: RPC = Rpc.from((ctx: Context));
|
177 | ```
|
178 |
|
179 | - `ctx: Context` - Required. A
|
180 | [Fusion.js context](https://github.com/fusionjs/fusion-core#context)
|
181 | - returns `rpc: {request: (method: string, args: any) => Promise<any>}`
|
182 |
|
183 | - `request: (method: string, args: any) => Promise<any>` - Makes an RPC call
|
184 | via an HTTP request. If on the server, this will directly call the `method`
|
185 | handler with `(args, ctx)`.
|
186 |
|
187 | If on the browser, this will `POST` to `/api/${method}` endpoint with JSON
|
188 | serialized args as the request body. The server will then deserialize the
|
189 | args and call the rpc handler. The response will be serialized and send back
|
190 | to the browser.
|
191 |
|
192 | - `method: string` - Required. The RPC method name
|
193 | - `args: any` - Optional. Arguments to pass to the server-side RPC handler.
|
194 | Must be JSON-serializable.
|
195 |
|
196 | ### mock
|
197 |
|
198 | The package also exports a mock RPC plugin which can be useful for testing. For
|
199 | example:
|
200 |
|
201 | ```js
|
202 | import {mock as MockRPC, RPCToken} from 'fusion-plugin-rpc';
|
203 |
|
204 | app.register(RPCToken, mock);
|
205 | ```
|
206 |
|
207 | ### Error Handling
|
208 |
|
209 | Use the `ResponseError` error subclass for sending error responses. If this
|
210 | error class is not used, a generic message will be sent to the client.
|
211 |
|
212 | ```js
|
213 | import {ResponseError} from 'fusion-plugin-rpc';
|
214 |
|
215 | function testHandler() {
|
216 | try {
|
217 | doThing();
|
218 | } catch (e) {
|
219 | const error = new ResponseError('Failed to do thing');
|
220 | error.code = 'DOTHING';
|
221 | error.meta = {
|
222 | custom: 'metadata',
|
223 | };
|
224 | throw error;
|
225 | }
|
226 | }
|
227 | ```
|
228 |
|
229 | ### Generating mock RPC handlers from fixtures
|
230 |
|
231 | The package also exports a getMockRpcHandlers util which can be useful for testing.
|
232 | Fixtures need to be of the following type
|
233 |
|
234 | ```js
|
235 | type RpcResponse = Object | ResponseError;
|
236 | type RpcResponseMap = Array<{
|
237 | args: Array<*>,
|
238 | response: RpcResponse,
|
239 | }>;
|
240 | type RpcFixtureT = {[string]: RpcResponseMap | RpcResponse};
|
241 | ```
|
242 |
|
243 | `getMockRpcHandlers` has the following interface:
|
244 |
|
245 | ```js
|
246 | type getMockRpcHandlersT = (
|
247 | fixtures: Array<RpcFixtureT>,
|
248 | onMockRpc?: OnMockRpcCallbackT
|
249 | ) => HandlerType;
|
250 | ```
|
251 |
|
252 | For example:
|
253 |
|
254 | ```js
|
255 | import {getMockRpcHandlers, ResponseError} from 'fusion-plugin-rpc';
|
256 |
|
257 | const rpcFixtures = [
|
258 | {
|
259 | getUser: {
|
260 | firstName: 'John',
|
261 | lastName: 'Doe',
|
262 | uuid: 123,
|
263 | },
|
264 | },
|
265 | {
|
266 | updateUser: [{
|
267 | args: [{firstName: 'Jane'}],
|
268 | response: {
|
269 | firstName: 'John',
|
270 | lastName: 'Doe',
|
271 | uuid: 123,
|
272 | },
|
273 | }, {
|
274 | args: [{firstName: ''}],
|
275 | response: new ResponseError('Username cant be empty'),
|
276 | }]
|
277 | },
|
278 | ];
|
279 |
|
280 | const mockRpcHandlers = getMockRpcHandlers(rpcFixtures);
|
281 |
|
282 | const user = await mockRpcHandlers.getUser();
|
283 |
|
284 | try {
|
285 | const user = await mockRpcHandlers.updateUser({firstName: ''});
|
286 | } catch (updatedUserError) {
|
287 | // When error object is passed as response in fixtures,
|
288 | // it will be considered as a failure scenario and will be thrown by rpc handler.
|
289 | }
|
290 | ```
|