UNPKG

15 kBPlain TextView Raw
1import express from "express";
2import bodyParser from "body-parser";
3import cookieParser from "cookie-parser";
4import {get, isNil, set, defaultsDeep, each, castArray, has, isNaN} from "lodash";
5import {APIConfig} from "./lib/APIConfig";
6import path from "path";
7import {APIUtils} from "./lib/APIUtils";
8import "reflect-metadata";
9import {APIResponse} from "./lib/APIResponse";
10import {APIError} from "./lib/APIError";
11import {KVService} from "./lib/Services/KeyValue/KVService";
12import {FileService} from "./lib/Services/File/FileService";
13import {EnvVarSync} from "./lib/Services/Config";
14import {APIAuthUser, APIAuthUtils} from "./lib/APIAuthUtils";
15
16interface HandlerParameterData {
17 paramRawType: string;
18 paramType: string;
19 paramName: string;
20 paramOptions: APIParameterOptions;
21}
22
23interface HandlerData {
24 isInstance: boolean;
25 options: APIEndpointOptions;
26 handlerFunction: Function;
27 handlerParameterNames: string[];
28 handlerParameterData: { [paramIndex: number]: HandlerParameterData };
29}
30
31export interface APILoaderDefinition {
32 apiPath?: string;
33 require: string;
34 moduleName?: string;
35}
36
37export interface APILoveOptions {
38
39 // One or more APIs to allow apilove to load. Remember these are lazy-loaded.
40 apis?: APILoaderDefinition[];
41
42 // By default cookieParser and bodyParser will be loaded. You can set this to false to prevent those from loading. Defaults to true.
43 loadStandardMiddleware?: boolean;
44
45 // Any other express.js middleware you want loaded before requests make it to apilove.
46 middleware?: any[];
47
48 // Override default express.js and APILove error handling
49 defaultErrorHandler?: (error, req, res, next) => void;
50
51 // This can be used to provide a default output for all requests. Useful to return a 404 or other default page.
52 defaultRouteHandler?: (req, res) => void;
53
54 callbackWaitsForEmptyEventLoop?: boolean;
55}
56
57function _createHandlerWrapperFunction(handlerData: HandlerData, thisObject) {
58 return (req, res, next) => {
59
60 let apiResponse = new APIResponse(req, res, next);
61
62 // Does this require authentication?
63 if (handlerData.options.requireAuthentication) {
64 if (!req.auth || !req.auth.isAuthenticated || req.auth.isExpired) {
65 apiResponse.withError(APIError.create401UnauthorizedError());
66 return;
67 }
68 }
69
70 let handlerArgs = [];
71 let validationErrors: { parameter: string, message: string }[] = [];
72
73 // Loop through each parameter in our function and pull it from the request
74 for (let index = 0; index < handlerData.handlerParameterNames.length; index++) {
75 let paramData: HandlerParameterData = handlerData.handlerParameterData[index];
76 let paramOptions: APIParameterOptions = get(paramData, "paramOptions", {});
77 let paramName = APIUtils.coalesce(paramOptions.rawName, handlerData.handlerParameterNames[index]);
78
79 // Ignore request and response parameters if the function asks for it
80 if ((index === handlerData.handlerParameterNames.length - 1 || index === handlerData.handlerParameterNames.length - 2) && ["req", "request", "res", "response"].indexOf(paramName.toLowerCase()) >= 0) {
81 continue;
82 }
83
84 let paramSources: string[] = castArray(get(paramOptions, "sources", ["params", "query", "body", "cookie", "headers"]));
85 let paramValue;
86
87 if (req.auth && paramData.paramType === "APIAuthUser") {
88 paramValue = APIAuthUtils.getAPIAuthUserFromAuthCredentials(req.auth);
89 } else {
90 for (let paramSource of paramSources) {
91 let paramValues = get(req, paramSource);
92
93 if (isNil(paramValues)) {
94 continue;
95 }
96
97 if (paramOptions.includeFullSource ||
98 (/[\.\[\]]/g).test(paramSource) // If the source contains any of the characters ".[]" (ie a path), assume the developer meant to include the full source.
99 ) {
100 paramValue = paramValues;
101 break;
102 } else {
103 if (has(paramValues, paramName)) {
104 paramValue = paramValues[paramName];
105 break;
106 }
107 }
108 }
109 }
110
111 let argValue = APIUtils.coalesce(paramValue, paramOptions.defaultValue);
112
113 if (paramOptions.processor) {
114 try {
115 argValue = paramOptions.processor(argValue, req);
116 } catch (error) {
117 validationErrors.push({
118 parameter: paramName,
119 message: error.message || error.toString()
120 });
121 continue;
122 }
123 }
124
125 if (isNil(argValue)) {
126
127 // Is this parameter required?
128 if (!get(paramOptions, "optional", false)) {
129 validationErrors.push({
130 parameter: paramName,
131 message: "missing"
132 });
133 }
134
135 handlerArgs.push(undefined);
136 continue;
137 }
138
139 argValue = APIUtils.convertToType(argValue, paramData.paramRawType);
140
141 if (isNil(argValue) || isNaN(argValue)) {
142 validationErrors.push({
143 parameter: paramName,
144 message: "invalid"
145 });
146 continue;
147 }
148
149 handlerArgs.push(argValue);
150 }
151
152 if (validationErrors.length > 0) {
153 apiResponse.withError(APIError.createValidationError(validationErrors));
154 return;
155 }
156
157 apiResponse.processHandlerFunction(thisObject, handlerData.handlerFunction, handlerArgs, handlerData.options.disableFriendlyResponse, handlerData.options.successResponse);
158 };
159}
160
161function _loadAPI(apiRouter, apiDefinition: APILoaderDefinition) {
162
163 let apiModule;
164 try {
165 apiModule = require(path.resolve(process.cwd(), apiDefinition.require));
166 } catch (e) {
167 console.error(e);
168 return null;
169 }
170
171 if (isNil(apiModule)) {
172 return null;
173 }
174
175 let moduleName = APIUtils.coalesce(apiDefinition.moduleName, path.basename(apiDefinition.require));
176 let apiClass = APIUtils.coalesce(apiModule[moduleName], apiModule.default, apiModule);
177
178 let apiInstance;
179
180 each(get(apiClass, "__handlerData", {}), (handlerData: HandlerData, name) => {
181
182 // If this is an instance function, we need to create an instance of the class
183 if (handlerData.isInstance && isNil(apiInstance)) {
184 apiInstance = new apiClass();
185 }
186
187 let options: APIEndpointOptions = handlerData.options;
188 let argsArray: any[] = [options.path];
189
190
191 if (options.middleware) {
192 argsArray = argsArray.concat(castArray(options.middleware));
193 }
194
195 let handlerWrapper = _createHandlerWrapperFunction(handlerData, handlerData.isInstance ? apiInstance : apiClass);
196 argsArray.push(handlerWrapper);
197
198 apiRouter[options.method.toLowerCase()].apply(apiRouter, argsArray);
199 });
200}
201
202export class APILove {
203
204 static app = express();
205
206 static start(options: APILoveOptions) {
207
208 if (options.loadStandardMiddleware !== false) {
209 this.app.use(cookieParser());
210 this.app.use(bodyParser.json({limit: "50mb"}));
211 this.app.use(bodyParser.urlencoded({limit: "50mb", extended: false, parameterLimit: 50000}));
212 this.app.use(bodyParser.text({limit: "50mb"}));
213 this.app.use((req, res, next) => {
214 req.auth = APIAuthUtils.getAuthCredentialsFromRequest(req, true);
215 next();
216 });
217 }
218
219 for (let mw of get(options, "middleware", [])) {
220 this.app.use(mw);
221 }
222
223 // Here we load our APIs, but we only load them when requested
224 for (let api of get(options, "apis", []) as APILoaderDefinition[]) {
225
226 if (isNil(api.apiPath)) {
227 api.apiPath = "";
228 }
229
230 if (APIConfig.LAZY_LOAD_APIS) {
231 let apiRouter;
232
233 this.app.use(api.apiPath, (req, res, next) => {
234
235 // Lazy load our API
236 if (!apiRouter) {
237 apiRouter = express.Router();
238 _loadAPI(apiRouter, api);
239 }
240
241 apiRouter(req, res, next);
242 });
243 } else {
244 let apiRouter = express.Router();
245 _loadAPI(apiRouter, api);
246 this.app.use(api.apiPath, apiRouter);
247 }
248 }
249
250 if (!isNil(options.defaultRouteHandler)) {
251 this.app.use(options.defaultRouteHandler);
252 }
253
254 // Setup our default error handler
255 if (!isNil(options.defaultErrorHandler)) {
256 this.app.use(options.defaultErrorHandler);
257 } else {
258 this.app.use((error, req, res, next) => {
259
260 if (error instanceof APIError) {
261 let apiError = error as APIError;
262 res.status(apiError.statusCode).send(APIConfig.OUTPUT_HAPI_RESULTS ? apiError.hapiOut() : apiError.out());
263 } else {
264 let apiResponse = new APIResponse(res, res);
265 apiResponse.withError(error);
266 }
267
268 });
269 }
270
271
272 if (APIConfig.RUN_AS_SERVER) {
273 this.app.listen(APIConfig.WEB_PORT, () => console.log(`API listening on port ${APIConfig.WEB_PORT}`));
274 return this.app;
275 } else {
276 let serverless = require("serverless-http");
277 return serverless(this.app, {callbackWaitsForEmptyEventLoop: !!options.callbackWaitsForEmptyEventLoop});
278 }
279 }
280}
281
282export interface APIParameterOptions {
283
284 /**
285 * If set to true, an error will not be thrown to the API caller if the value is not sent
286 */
287 optional?: boolean;
288
289 /**
290 * A default value to be used if one can't be found. This would be an equivalent shortcut for setting optional=true and providing a default value for your method property
291 */
292 defaultValue?: any;
293
294 /**
295 * A synchronous function that can be used to transform an incoming parameter into something else. Can also be used as validation by throwing an error.
296 * You also get access to the raw express.js req object if you want it.
297 */
298 processor?: (value: any, req?) => any;
299
300 /**
301 * One or more sources from which to look for this value. This is basically a path in the req object. So for example, a value of `query` would be equivalent to `req.query[myParamName]`
302 * Multiple values can be defined, and whichever one results in a non-null value first will be used. Defaults to ["params", "query", "body", "cookie", "headers"].
303 */
304 sources?: string[] | string;
305
306 /**
307 * If set to true, the entire source will be returned instead of looking for a particular value. Defaults to false.
308 *
309 * Examples:
310 *
311 * The following would look for something named `userData` in the query params and return that.
312 * @APIParameter({sources:["query"]})
313 * userData:string
314 *
315 * The following would take all the query params and return them as an object
316 * @APIParameter({sources:["query"], includeFullSource:true})
317 * userData:{[paramName:string] : any}
318 */
319 includeFullSource?: boolean;
320
321 /**
322 * This is the raw name of the parameter to look for in cases where the name can't be represented as a valid javascript variable name.
323 * Examples usages might be when looking for a header like "content-type" or a parameter named "function"
324 */
325 rawName?: string;
326}
327
328export function APIParameter(options: APIParameterOptions) {
329 return function (target, key, parameterIndex: number) {
330 let isInstance = isNil(target.prototype);
331 let theClass = isInstance ? target.constructor : target.prototype.constructor;
332
333 let handlerData: HandlerData = get(theClass, `__handlerData.${key}`, {});
334 set(handlerData, `handlerParameterData.${parameterIndex}.paramOptions`, options);
335 set(theClass, `__handlerData.${key}`, handlerData);
336 }
337}
338
339export interface APIEndpointOptions {
340 // The method to be used when requesting this endpoint. Defaults to "get".
341 method?: string;
342
343 // The path to reach this endpoint. Defaults to "/".
344 path?: string;
345
346 // Any express.js middleware functions you want to be executed before invoking this method. Useful for things like authentication.
347 middleware?: ((req, res, next?) => void)[] | ((req, res, next) => void);
348
349 // Turn this on if you want to return data as-is and not in HAPI format
350 disableFriendlyResponse?: boolean;
351
352 // Specify a function here to handle the response yourself
353 successResponse?: (responseData: any, res) => void;
354
355 // If set to true, a valid JWT must be present in the request, otherwise a 401 error will be thrown
356 requireAuthentication?: boolean;
357}
358
359export function APIEndpoint(options?: APIEndpointOptions) {
360 return function (target, key, descriptor) {
361
362 let isInstance = isNil(target.prototype);
363 let theClass = isInstance ? target.constructor : target.prototype.constructor;
364
365 let handlerData: HandlerData = get(theClass, `__handlerData.${key}`, {});
366
367 options = defaultsDeep({}, options, {
368 method: "get",
369 path: "/"
370 });
371
372 let parameterMetadata = Reflect.getMetadata("design:paramtypes", target, key);
373 let parameterNames = APIUtils.getFunctionParamNames(descriptor.value);
374
375 handlerData.isInstance = isInstance;
376 handlerData.handlerFunction = descriptor.value;
377 handlerData.options = options;
378 handlerData.handlerParameterNames = parameterNames;
379
380 for (let parameterIndex = 0; parameterIndex < parameterNames.length; parameterIndex++) {
381 set(handlerData, `handlerParameterData.${parameterIndex}.paramRawType`, APIUtils.getRawTypeName(parameterMetadata[parameterIndex].prototype));
382 set(handlerData, `handlerParameterData.${parameterIndex}.paramType`, parameterMetadata[parameterIndex].name);
383 set(handlerData, `handlerParameterData.${parameterIndex}.paramName`, parameterNames[parameterIndex]);
384 }
385
386 set(theClass, `__handlerData.${key}`, handlerData);
387 }
388}
389
390// Re-export stuff
391// TODO: do we need to reconsider this? Is this causing unneeded memory usage if none of these end up getting used?
392export {
393 APIConfig,
394 APIAuthUtils,
395 APIError,
396 APIResponse,
397 APIUtils,
398 KVService as APIKVService,
399 FileService as APIFileService,
400 EnvVarSync
401};
\No newline at end of file