UNPKG

15.2 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
106 if(paramSource === "query")
107 {
108 paramValue = decodeURIComponent(paramValue);
109 }
110
111 break;
112 }
113 }
114 }
115 }
116
117 let argValue = APIUtils.coalesce(paramValue, paramOptions.defaultValue);
118
119 if (paramOptions.processor) {
120 try {
121 argValue = paramOptions.processor(argValue, req);
122 } catch (error) {
123 validationErrors.push({
124 parameter: paramName,
125 message: error.message || error.toString()
126 });
127 continue;
128 }
129 }
130
131 if (isNil(argValue)) {
132
133 // Is this parameter required?
134 if (!get(paramOptions, "optional", false)) {
135 validationErrors.push({
136 parameter: paramName,
137 message: "missing"
138 });
139 }
140
141 handlerArgs.push(undefined);
142 continue;
143 }
144
145 argValue = APIUtils.convertToType(argValue, paramData.paramRawType);
146
147 if (isNil(argValue) || isNaN(argValue)) {
148 validationErrors.push({
149 parameter: paramName,
150 message: "invalid"
151 });
152 continue;
153 }
154
155 handlerArgs.push(argValue);
156 }
157
158 if (validationErrors.length > 0) {
159 apiResponse.withError(APIError.createValidationError(validationErrors));
160 return;
161 }
162
163 apiResponse.processHandlerFunction(thisObject, handlerData.handlerFunction, handlerArgs, handlerData.options.disableFriendlyResponse, handlerData.options.successResponse);
164 };
165}
166
167function _loadAPI(apiRouter, apiDefinition: APILoaderDefinition) {
168
169 let apiModule;
170 try {
171 apiModule = require(path.resolve(process.cwd(), apiDefinition.require));
172 } catch (e) {
173 console.error(e);
174 return null;
175 }
176
177 if (isNil(apiModule)) {
178 return null;
179 }
180
181 let moduleName = APIUtils.coalesce(apiDefinition.moduleName, path.basename(apiDefinition.require));
182 let apiClass = APIUtils.coalesce(apiModule[moduleName], apiModule.default, apiModule);
183
184 let apiInstance;
185
186 each(get(apiClass, "__handlerData", {}), (handlerData: HandlerData, name) => {
187
188 // If this is an instance function, we need to create an instance of the class
189 if (handlerData.isInstance && isNil(apiInstance)) {
190 apiInstance = new apiClass();
191 }
192
193 let options: APIEndpointOptions = handlerData.options;
194 let argsArray: any[] = [options.path];
195
196
197 if (options.middleware) {
198 argsArray = argsArray.concat(castArray(options.middleware));
199 }
200
201 let handlerWrapper = _createHandlerWrapperFunction(handlerData, handlerData.isInstance ? apiInstance : apiClass);
202 argsArray.push(handlerWrapper);
203
204 apiRouter[options.method.toLowerCase()].apply(apiRouter, argsArray);
205 });
206}
207
208export class APILove {
209
210 static app = express();
211
212 static start(options: APILoveOptions) {
213
214 if (options.loadStandardMiddleware !== false) {
215 this.app.use(cookieParser());
216 this.app.use(bodyParser.json({limit: "50mb"}));
217 this.app.use(bodyParser.urlencoded({limit: "50mb", extended: false, parameterLimit: 50000}));
218 this.app.use(bodyParser.text({limit: "50mb"}));
219 this.app.use((req, res, next) => {
220 req.auth = APIAuthUtils.getAuthCredentialsFromRequest(req, true);
221 next();
222 });
223 }
224
225 for (let mw of get(options, "middleware", [])) {
226 this.app.use(mw);
227 }
228
229 // Here we load our APIs, but we only load them when requested
230 for (let api of get(options, "apis", []) as APILoaderDefinition[]) {
231
232 if (isNil(api.apiPath)) {
233 api.apiPath = "";
234 }
235
236 if (APIConfig.LAZY_LOAD_APIS) {
237 let apiRouter;
238
239 this.app.use(api.apiPath, (req, res, next) => {
240
241 // Lazy load our API
242 if (!apiRouter) {
243 apiRouter = express.Router();
244 _loadAPI(apiRouter, api);
245 }
246
247 apiRouter(req, res, next);
248 });
249 } else {
250 let apiRouter = express.Router();
251 _loadAPI(apiRouter, api);
252 this.app.use(api.apiPath, apiRouter);
253 }
254 }
255
256 if (!isNil(options.defaultRouteHandler)) {
257 this.app.use(options.defaultRouteHandler);
258 }
259
260 // Setup our default error handler
261 if (!isNil(options.defaultErrorHandler)) {
262 this.app.use(options.defaultErrorHandler);
263 } else {
264 this.app.use((error, req, res, next) => {
265
266 if (error instanceof APIError) {
267 let apiError = error as APIError;
268 res.status(apiError.statusCode).send(APIConfig.OUTPUT_HAPI_RESULTS ? apiError.hapiOut() : apiError.out());
269 } else {
270 let apiResponse = new APIResponse(res, res);
271 apiResponse.withError(error);
272 }
273
274 });
275 }
276
277
278 if (APIConfig.RUN_AS_SERVER) {
279 this.app.listen(APIConfig.WEB_PORT, () => console.log(`API listening on port ${APIConfig.WEB_PORT}`));
280 return this.app;
281 } else {
282 let serverless = require("serverless-http");
283 return serverless(this.app, {callbackWaitsForEmptyEventLoop: !!options.callbackWaitsForEmptyEventLoop});
284 }
285 }
286}
287
288export interface APIParameterOptions {
289
290 /**
291 * If set to true, an error will not be thrown to the API caller if the value is not sent
292 */
293 optional?: boolean;
294
295 /**
296 * 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
297 */
298 defaultValue?: any;
299
300 /**
301 * 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.
302 * You also get access to the raw express.js req object if you want it.
303 */
304 processor?: (value: any, req?) => any;
305
306 /**
307 * 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]`
308 * 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"].
309 */
310 sources?: string[] | string;
311
312 /**
313 * If set to true, the entire source will be returned instead of looking for a particular value. Defaults to false.
314 *
315 * Examples:
316 *
317 * The following would look for something named `userData` in the query params and return that.
318 * @APIParameter({sources:["query"]})
319 * userData:string
320 *
321 * The following would take all the query params and return them as an object
322 * @APIParameter({sources:["query"], includeFullSource:true})
323 * userData:{[paramName:string] : any}
324 */
325 includeFullSource?: boolean;
326
327 /**
328 * 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.
329 * Examples usages might be when looking for a header like "content-type" or a parameter named "function"
330 */
331 rawName?: string;
332}
333
334export function APIParameter(options: APIParameterOptions) {
335 return function (target, key, parameterIndex: number) {
336 let isInstance = isNil(target.prototype);
337 let theClass = isInstance ? target.constructor : target.prototype.constructor;
338
339 let handlerData: HandlerData = get(theClass, `__handlerData.${key}`, {});
340 set(handlerData, `handlerParameterData.${parameterIndex}.paramOptions`, options);
341 set(theClass, `__handlerData.${key}`, handlerData);
342 }
343}
344
345export interface APIEndpointOptions {
346 // The method to be used when requesting this endpoint. Defaults to "get".
347 method?: string;
348
349 // The path to reach this endpoint. Defaults to "/".
350 path?: string;
351
352 // Any express.js middleware functions you want to be executed before invoking this method. Useful for things like authentication.
353 middleware?: ((req, res, next?) => void)[] | ((req, res, next) => void);
354
355 // Turn this on if you want to return data as-is and not in HAPI format
356 disableFriendlyResponse?: boolean;
357
358 // Specify a function here to handle the response yourself
359 successResponse?: (responseData: any, res) => void;
360
361 // If set to true, a valid JWT must be present in the request, otherwise a 401 error will be thrown
362 requireAuthentication?: boolean;
363}
364
365export function APIEndpoint(options?: APIEndpointOptions) {
366 return function (target, key, descriptor) {
367
368 let isInstance = isNil(target.prototype);
369 let theClass = isInstance ? target.constructor : target.prototype.constructor;
370
371 let handlerData: HandlerData = get(theClass, `__handlerData.${key}`, {});
372
373 options = defaultsDeep({}, options, {
374 method: "get",
375 path: "/"
376 });
377
378 let parameterMetadata = Reflect.getMetadata("design:paramtypes", target, key);
379 let parameterNames = APIUtils.getFunctionParamNames(descriptor.value);
380
381 handlerData.isInstance = isInstance;
382 handlerData.handlerFunction = descriptor.value;
383 handlerData.options = options;
384 handlerData.handlerParameterNames = parameterNames;
385
386 for (let parameterIndex = 0; parameterIndex < parameterNames.length; parameterIndex++) {
387 set(handlerData, `handlerParameterData.${parameterIndex}.paramRawType`, APIUtils.getRawTypeName(parameterMetadata[parameterIndex].prototype));
388 set(handlerData, `handlerParameterData.${parameterIndex}.paramType`, parameterMetadata[parameterIndex].name);
389 set(handlerData, `handlerParameterData.${parameterIndex}.paramName`, parameterNames[parameterIndex]);
390 }
391
392 set(theClass, `__handlerData.${key}`, handlerData);
393 }
394}
395
396// Re-export stuff
397// TODO: do we need to reconsider this? Is this causing unneeded memory usage if none of these end up getting used?
398export {
399 APIConfig,
400 APIAuthUtils,
401 APIError,
402 APIResponse,
403 APIUtils,
404 KVService as APIKVService,
405 FileService as APIFileService,
406 EnvVarSync
407};
\No newline at end of file