UNPKG

6.18 kBJavaScriptView Raw
1/** Copyright (c) 2018 Uber Technologies, Inc.
2 *
3 * This source code is licensed under the MIT license found in the
4 * LICENSE file in the root directory of this source tree.
5 *
6 * @flow
7 */
8
9/* eslint-env node */
10
11import bodyparser from 'koa-bodyparser';
12
13import {createPlugin, memoize} from 'fusion-core';
14import type {Context} from 'fusion-core';
15import {UniversalEventsToken} from 'fusion-plugin-universal-events';
16import type {Fetch} from 'fusion-tokens';
17
18import MissingHandlerError from './missing-handler-error';
19import ResponseError from './response-error';
20import {
21 BodyParserOptionsToken,
22 RPCHandlersToken,
23 RPCHandlersConfigToken,
24} from './tokens.js';
25import type {HandlerType} from './tokens.js';
26import type {RPCPluginType, IEmitter} from './types.js';
27import {formatApiPath} from './utils.js';
28
29const statKey = 'rpc:method';
30
31/* Helper function */
32function hasHandler(handlers: HandlerType, method: string): boolean {
33 return handlers.hasOwnProperty(method);
34}
35
36class RPC {
37 ctx: ?Context;
38 emitter: ?IEmitter;
39 handlers: ?HandlerType;
40 fetch: ?Fetch;
41
42 constructor(emitter: IEmitter, handlers: any, ctx: Context): RPC {
43 if (!ctx || !ctx.headers) {
44 throw new Error('fusion-plugin-rpc requires `ctx`');
45 }
46 this.ctx = ctx;
47 this.emitter = emitter;
48 this.handlers = handlers;
49
50 return this;
51 }
52
53 async request<TArgs, TResult>(method: string, args: TArgs): Promise<TResult> {
54 const startTime = ms();
55
56 if (!this.ctx) {
57 throw new Error('fusion-plugin-rpc requires `ctx`');
58 }
59 if (!this.emitter) {
60 throw new Error('fusion-plugin-rpc requires `emitter`');
61 }
62 const scopedEmitter = this.emitter.from(this.ctx);
63
64 if (!this.handlers) {
65 throw new Error('fusion-plugin-rpc requires `handlers`');
66 }
67 if (!hasHandler(this.handlers, method)) {
68 const e = new MissingHandlerError(method);
69 if (scopedEmitter) {
70 scopedEmitter.emit('rpc:error', {
71 method,
72 origin: 'server',
73 error: e,
74 });
75 }
76 throw e;
77 }
78 try {
79 const result = await this.handlers[method](args, this.ctx);
80 if (scopedEmitter) {
81 scopedEmitter.emit(statKey, {
82 method,
83 status: 'success',
84 origin: 'server',
85 timing: ms() - startTime,
86 });
87 }
88 return result;
89 } catch (e) {
90 if (scopedEmitter) {
91 scopedEmitter.emit(statKey, {
92 method,
93 error: e,
94 status: 'failure',
95 origin: 'server',
96 timing: ms() - startTime,
97 });
98 }
99 throw e;
100 }
101 }
102}
103
104const pluginFactory: () => RPCPluginType = () =>
105 createPlugin({
106 deps: {
107 emitter: UniversalEventsToken,
108 handlers: RPCHandlersToken,
109 bodyParserOptions: BodyParserOptionsToken.optional,
110 rpcConfig: RPCHandlersConfigToken.optional,
111 },
112
113 provides: deps => {
114 const {emitter, handlers} = deps;
115
116 const service = {
117 from: memoize(ctx => new RPC(emitter, handlers, ctx)),
118 };
119 return service;
120 },
121
122 middleware: deps => {
123 const {emitter, handlers, bodyParserOptions, rpcConfig} = deps;
124 if (!handlers)
125 throw new Error('Missing handlers registered to RPCHandlersToken');
126 if (!emitter)
127 throw new Error('Missing emitter registered to UniversalEventsToken');
128 const parseBody = bodyparser(bodyParserOptions);
129
130 const apiPath = formatApiPath(
131 rpcConfig && rpcConfig.apiPath ? rpcConfig.apiPath : 'api'
132 );
133
134 return async (ctx, next) => {
135 await next();
136 const scopedEmitter = emitter.from(ctx);
137 if (ctx.method === 'POST' && ctx.path.startsWith(apiPath)) {
138 const startTime = ms();
139 // eslint-disable-next-line no-useless-escape
140 const pathMatch = new RegExp(`${apiPath}([^/]+)`, 'i');
141 const [, method] = ctx.path.match(pathMatch) || [];
142 if (hasHandler(handlers, method)) {
143 await parseBody(ctx, () => Promise.resolve());
144 try {
145 const result = await handlers[method](ctx.request.body, ctx);
146 ctx.body = {
147 status: 'success',
148 data: result,
149 };
150 if (scopedEmitter) {
151 scopedEmitter.emit(statKey, {
152 method,
153 status: 'success',
154 origin: 'browser',
155 timing: ms() - startTime,
156 });
157 }
158 } catch (e) {
159 const error =
160 e instanceof ResponseError
161 ? e
162 : new Error(
163 'UnknownError - Use ResponseError from fusion-plugin-rpc (or fusion-plugin-rpc-redux-react if you are using React) package for more detailed error messages'
164 );
165 ctx.body = {
166 status: 'failure',
167 data: {
168 message: error.message,
169 // $FlowFixMe
170 code: error.code,
171 // $FlowFixMe
172 meta: error.meta,
173 },
174 };
175 if (scopedEmitter) {
176 scopedEmitter.emit(statKey, {
177 method,
178 error: e,
179 status: 'failure',
180 origin: 'browser',
181 timing: ms() - startTime,
182 });
183 }
184 }
185 } else {
186 const e = new MissingHandlerError(method);
187 ctx.body = {
188 status: 'failure',
189 data: {
190 message: e.message,
191 code: e.code,
192 },
193 };
194 ctx.status = 404;
195 if (scopedEmitter) {
196 scopedEmitter.emit('rpc:error', {
197 origin: 'browser',
198 method,
199 error: e,
200 });
201 }
202 }
203 }
204 };
205 },
206 });
207
208/* Helper functions */
209function ms() {
210 const [seconds, ns] = process.hrtime();
211 return Math.round(seconds * 1000 + ns / 1e6);
212}
213
214export default ((__NODE__ && pluginFactory(): any): RPCPluginType);