UNPKG

12.4 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const core_1 = require("@plumjs/core");
5const reflect_1 = require("@plumjs/reflect");
6const chalk_1 = tslib_1.__importDefault(require("chalk"));
7const debug_1 = tslib_1.__importDefault(require("debug"));
8const Fs = tslib_1.__importStar(require("fs"));
9const Path = tslib_1.__importStar(require("path"));
10const path_to_regexp_1 = tslib_1.__importDefault(require("path-to-regexp"));
11const log = debug_1.default("plum:router");
12/* ------------------------------------------------------------------------------- */
13/* ------------------------------- HELPERS --------------------------------------- */
14/* ------------------------------------------------------------------------------- */
15function striveController(name) {
16 return name.substring(0, name.lastIndexOf("Controller")).toLowerCase();
17}
18exports.striveController = striveController;
19function getControllerRoute(controller) {
20 const root = controller.decorators.find((x) => x.name == "Root");
21 return (root && root.url) || `/${striveController(controller.name)}`;
22}
23exports.getControllerRoute = getControllerRoute;
24function getActionName(route) {
25 return `${route.controller.name}.${route.action.name}(${route.action.parameters.map(x => x.name).join(", ")})`;
26}
27function resolveDir(path, ext) {
28 //resolve provided path directory or file
29 if (Fs.lstatSync(path).isDirectory()) {
30 const files = Fs.readdirSync(path)
31 //take only file in extension list
32 .filter(x => ext.some(ex => Path.extname(x) === ex))
33 //add root path + file name
34 .map(x => Path.join(path, x));
35 log(`[Router] Resolve files with ${ext.join("|")}`);
36 log(`${files.join("\n")}`);
37 return files;
38 }
39 else
40 return [path];
41}
42/* ------------------------------------------------------------------------------- */
43/* ---------------------------------- TRANSFORMER -------------------------------- */
44/* ------------------------------------------------------------------------------- */
45function transformRouteDecorator(controller, method) {
46 if (method.decorators.some((x) => x.name == "Ignore"))
47 return;
48 const root = getControllerRoute(controller);
49 const decorator = method.decorators.find((x) => x.name == "Route");
50 const result = { action: method, method: decorator.method, controller: controller };
51 //absolute route
52 if (decorator.url && decorator.url.startsWith("/"))
53 return Object.assign({}, result, { url: decorator.url });
54 //empty string
55 else if (decorator.url === "")
56 return Object.assign({}, result, { url: root });
57 //relative route
58 else {
59 const actionUrl = decorator.url || method.name.toLowerCase();
60 return Object.assign({}, result, { url: [root, actionUrl].join("/") });
61 }
62}
63function transformRegular(controller, method) {
64 return {
65 method: "get",
66 url: `${getControllerRoute(controller)}/${method.name.toLowerCase()}`,
67 action: method,
68 controller: controller,
69 };
70}
71function transformController(object) {
72 const controller = typeof object === "function" ? reflect_1.reflect(object) : object;
73 if (!controller.name.toLowerCase().endsWith("controller"))
74 return [];
75 return controller.methods.map(method => {
76 //first priority is decorator
77 if (method.decorators.some((x) => x.name == "Ignore" || x.name == "Route"))
78 return transformRouteDecorator(controller, method);
79 else
80 return transformRegular(controller, method);
81 })
82 //ignore undefined
83 .filter(x => Boolean(x));
84}
85exports.transformController = transformController;
86function transformModule(path, extensions) {
87 //read all files and get module reflection
88 const modules = resolveDir(path, extensions)
89 //reflect the file
90 .map(x => reflect_1.reflect(x));
91 //get all module.members and combine into one array
92 return modules.reduce((a, b) => a.concat(b.members), [])
93 //take only the controller class
94 .filter(x => x.type === "Class" && x.name.toLowerCase().endsWith("controller"))
95 //traverse and change into route
96 .map(x => transformController(x))
97 //flatten the result
98 .flatten();
99}
100exports.transformModule = transformModule;
101/* ------------------------------------------------------------------------------- */
102/* ------------------------------- ROUTER ---------------------------------------- */
103/* ------------------------------------------------------------------------------- */
104function checkUrlMatch(route, ctx) {
105 const keys = [];
106 const regexp = path_to_regexp_1.default(route.url, keys);
107 const match = regexp.exec(ctx.path);
108 log(`[Router] Route: ${core_1.b(route.method)} ${core_1.b(route.url)} Ctx Path: ${core_1.b(ctx.method)} ${core_1.b(ctx.path)} Match: ${core_1.b(match)}`);
109 return { keys, match, method: route.method.toUpperCase(), route };
110}
111function router(infos, config, handler) {
112 return async (ctx, next) => {
113 const match = infos.map(x => checkUrlMatch(x, ctx))
114 .find(x => Boolean(x.match) && x.method == ctx.method);
115 if (match) {
116 log(`[Router] Match route ${core_1.b(match.route.method)} ${core_1.b(match.route.url)} with ${core_1.b(ctx.method)} ${core_1.b(ctx.path)}`);
117 //assign config and route to context
118 Object.assign(ctx, { config, route: match.route });
119 //add query
120 const query = match.keys.reduce((a, b, i) => {
121 a[b.name.toString().toLowerCase()] = match.match[i + 1];
122 return a;
123 }, {});
124 log(`[Router] Extracted parameter from url ${core_1.b(query)}`);
125 Object.assign(ctx.query, query);
126 await handler(ctx);
127 }
128 else {
129 log(`[Router] Not route match ${core_1.b(ctx.method)} ${core_1.b(ctx.url)}`);
130 await next();
131 }
132 };
133}
134exports.router = router;
135/* ------------------------------------------------------------------------------- */
136/* --------------------------- ANALYZER FUNCTION --------------------------------- */
137/* ------------------------------------------------------------------------------- */
138//------ Analyzer Helpers
139function getModelsInParameters(par) {
140 return par
141 .map((x, i) => ({ type: x.typeAnnotation, index: i }))
142 .filter(x => x.type && core_1.isCustomClass(x.type))
143 .map(x => ({ meta: reflect_1.reflect((Array.isArray(x.type) ? x.type[0] : x.type)), index: x.index }));
144}
145function traverseModel(par) {
146 const models = getModelsInParameters(par).map(x => x.meta);
147 const child = models.map(x => traverseModel(x.ctorParameters))
148 .filter((x) => Boolean(x))
149 .reduce((a, b) => a.concat(b), []);
150 return models.concat(child);
151}
152function traverseArray(parent, par) {
153 const models = getModelsInParameters(par);
154 if (models.length > 0) {
155 return models.map((x, i) => traverseArray(x.meta.name, x.meta.ctorParameters))
156 .flatten();
157 }
158 return par.filter(x => x.typeAnnotation === Array)
159 .map(x => `${parent}.${x.name}`);
160}
161//-----
162function backingParameterTest(route, allRoutes) {
163 const ids = route.url.split("/")
164 .filter(x => x.startsWith(":"))
165 .map(x => x.substring(1));
166 const missing = ids.filter(id => route.action.parameters.map(x => x.name).indexOf(id) === -1);
167 if (missing.length > 0) {
168 return {
169 type: "error",
170 message: core_1.errorMessage.RouteDoesNotHaveBackingParam.format(missing.join(", "))
171 };
172 }
173 else
174 return { type: "success" };
175}
176function metadataTypeTest(route, allRoutes) {
177 const hasTypeInfo = route.action
178 .parameters.some(x => Boolean(x.typeAnnotation));
179 if (!hasTypeInfo && route.action.parameters.length > 0) {
180 return {
181 type: "warning",
182 message: core_1.errorMessage.ActionDoesNotHaveTypeInfo
183 };
184 }
185 else
186 return { type: "success" };
187}
188function multipleDecoratorTest(route, allRoutes) {
189 const decorator = route.action.decorators.filter(x => x.name == "Route");
190 if (decorator.length > 1) {
191 return {
192 type: "error",
193 message: core_1.errorMessage.MultipleDecoratorNotSupported
194 };
195 }
196 else
197 return { type: "success" };
198}
199function duplicateRouteTest(route, allRoutes) {
200 const dup = allRoutes.filter(x => x.url == route.url && x.method == route.method);
201 if (dup.length > 1) {
202 return {
203 type: "error",
204 message: core_1.errorMessage.DuplicateRouteFound.format(dup.map(x => getActionName(x)).join(" "))
205 };
206 }
207 else
208 return { type: "success" };
209}
210function modelTypeInfoTest(route, allRoutes) {
211 const classes = traverseModel(route.action.parameters)
212 .filter(x => x.ctorParameters.every(par => typeof par.typeAnnotation == "undefined"))
213 .map(x => x.object);
214 log(`[Analyzer] Checking model types ${core_1.b(classes)}`);
215 //get only unique type
216 const noTypeInfo = Array.from(new Set(classes));
217 if (noTypeInfo.length > 0) {
218 log(`[Analyzer] Model without type information ${core_1.b(noTypeInfo.map(x => x.name).join(", "))}`);
219 return {
220 type: "warning",
221 message: core_1.errorMessage.ModelWithoutTypeInformation.format(noTypeInfo.map(x => x.name).join(", "))
222 };
223 }
224 else
225 return { type: "success" };
226}
227function arrayTypeInfoTest(route, allRoutes) {
228 const issues = traverseArray(`${route.controller.name}.${route.action.name}`, route.action.parameters);
229 const array = Array.from(new Set(issues));
230 if (array.length > 0) {
231 log(`[Analyzer] Array without item type information in ${array.join(", ")}`);
232 return {
233 type: "warning",
234 message: core_1.errorMessage.ArrayWithoutTypeInformation.format(array.join(", "))
235 };
236 }
237 else
238 return { type: 'success' };
239}
240/* ------------------------------------------------------------------------------- */
241/* -------------------------------- ANALYZER ------------------------------------- */
242/* ------------------------------------------------------------------------------- */
243function analyzeRoute(route, tests, allRoutes) {
244 const issues = tests.map(test => {
245 log(`[Analyzer] Analyzing using ${core_1.b(test.name)}`);
246 return test(route, allRoutes);
247 })
248 .filter(x => x.type != "success");
249 return { route, issues };
250}
251function analyzeRoutes(routes) {
252 log(`[Analyzer] Analysing ${core_1.b(routes.map(x => x.url).join(", "))}`);
253 const tests = [
254 backingParameterTest, metadataTypeTest, multipleDecoratorTest,
255 duplicateRouteTest, modelTypeInfoTest, arrayTypeInfoTest
256 ];
257 return routes.map(x => analyzeRoute(x, tests, routes));
258}
259exports.analyzeRoutes = analyzeRoutes;
260function printAnalysis(results) {
261 const data = results.map(x => {
262 const method = x.route.method.toUpperCase();
263 const action = getActionName(x.route);
264 const issues = x.issues.map(issue => ` - ${issue.type} ${issue.message}`);
265 return { method, url: x.route.url, action, issues };
266 });
267 console.log();
268 console.log(chalk_1.default.bold("Route Analysis Report"));
269 if (data.length == 0)
270 console.log("No controller found");
271 data.forEach((x, i) => {
272 const num = (i + 1).toString().padStart(data.length.toString().length);
273 const action = x.action.padEnd(Math.max(...data.map(x => x.action.length)));
274 const method = x.method.padEnd(Math.max(...data.map(x => x.method.length)));
275 //const url = x.url.padEnd(Math.max(...data.map(x => x.url.length)))
276 const issueColor = (issue) => issue.startsWith(" - warning") ? chalk_1.default.yellow(issue) : chalk_1.default.red(issue);
277 const color = x.issues.length == 0 ? (x) => x :
278 x.issues.some(x => x.startsWith(" - warning")) ? chalk_1.default.yellow : chalk_1.default.red;
279 console.log(color(`${num}. ${action} -> ${method} ${x.url}`));
280 x.issues.forEach(issue => console.log(issueColor(issue)));
281 });
282 if (data.length > 0)
283 console.log();
284}
285exports.printAnalysis = printAnalysis;