@rvoh/psychic 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/bin/helpers/printControllerHierarchy.js +168 -0
- package/dist/cjs/src/bin/index.js +7 -0
- package/dist/cjs/src/cli/index.js +25 -0
- package/dist/cjs/src/controller/index.js +14 -1
- package/dist/esm/src/bin/helpers/printControllerHierarchy.js +168 -0
- package/dist/esm/src/bin/index.js +7 -0
- package/dist/esm/src/cli/index.js +25 -0
- package/dist/esm/src/controller/index.js +14 -1
- package/dist/types/src/bin/helpers/printControllerHierarchy.d.ts +35 -0
- package/dist/types/src/bin/index.d.ts +2 -0
- package/dist/types/src/controller/index.d.ts +12 -1
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import colors from 'yoctocolors';
|
|
2
|
+
import PsychicController from '../../controller/index.js';
|
|
3
|
+
import PsychicApp from '../../psychic-app/index.js';
|
|
4
|
+
const ROOT_CHILD_INDENT = 5;
|
|
5
|
+
const MIN_DASHES = 2;
|
|
6
|
+
export default function printControllerHierarchy(controllersPath) {
|
|
7
|
+
const lines = controllerHierarchyLines(controllersPath);
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
console.log(line);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function resolveControllerClasses(controllersPath) {
|
|
13
|
+
const psychicApp = PsychicApp.getOrFail();
|
|
14
|
+
const resolvedPath = controllersPath ?? psychicApp.paths.controllers;
|
|
15
|
+
const allControllers = psychicApp.controllers;
|
|
16
|
+
const controllerClasses = Object.values(allControllers).filter(ctrl => {
|
|
17
|
+
return ctrl.globalName.startsWith(globalPrefixFromPath(resolvedPath, psychicApp.paths.controllers));
|
|
18
|
+
});
|
|
19
|
+
return { controllerClasses, resolvedPath };
|
|
20
|
+
}
|
|
21
|
+
export function controllerHierarchyLines(controllersPath) {
|
|
22
|
+
const { controllerClasses, resolvedPath } = resolveControllerClasses(controllersPath);
|
|
23
|
+
const childrenMap = buildChildrenMap(controllerClasses);
|
|
24
|
+
const roots = controllerClasses.filter(ctrl => !controllerClasses.some(other => other !== ctrl && ctrl.prototype instanceof other));
|
|
25
|
+
if (roots.length === 0) {
|
|
26
|
+
return ['No controllers found.'];
|
|
27
|
+
}
|
|
28
|
+
const lines = [];
|
|
29
|
+
for (const root of roots) {
|
|
30
|
+
collectTreeLines(root, childrenMap, { depth: 0, displayColumn: 0, strippedPrefix: '', baseIndent: 0 }, lines, resolvedPath);
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
export function controllerHierarchyViolations(controllersPath) {
|
|
35
|
+
const { controllerClasses, resolvedPath } = resolveControllerClasses(controllersPath);
|
|
36
|
+
const violations = [];
|
|
37
|
+
for (const ctrl of controllerClasses) {
|
|
38
|
+
const parentClass = Object.getPrototypeOf(ctrl);
|
|
39
|
+
if (parentClass === PsychicController)
|
|
40
|
+
continue;
|
|
41
|
+
const violation = hierarchyViolation(ctrl.globalName, parentClass.globalName, resolvedPath);
|
|
42
|
+
if (violation) {
|
|
43
|
+
violations.push(violation);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return violations;
|
|
47
|
+
}
|
|
48
|
+
export function buildChildrenMap(controllerClasses) {
|
|
49
|
+
const childrenMap = new Map();
|
|
50
|
+
for (const ctrl of controllerClasses) {
|
|
51
|
+
const parent = Object.getPrototypeOf(ctrl);
|
|
52
|
+
if (!childrenMap.has(parent)) {
|
|
53
|
+
childrenMap.set(parent, []);
|
|
54
|
+
}
|
|
55
|
+
childrenMap.get(parent).push(ctrl);
|
|
56
|
+
}
|
|
57
|
+
return childrenMap;
|
|
58
|
+
}
|
|
59
|
+
export function controllerTreeLine(displayedName, depth, displayColumn, baseIndent) {
|
|
60
|
+
if (depth === 0) {
|
|
61
|
+
return colors.cyan(displayedName);
|
|
62
|
+
}
|
|
63
|
+
const numDashes = Math.max(MIN_DASHES, displayColumn - baseIndent - 2);
|
|
64
|
+
const indentation = ' '.repeat(baseIndent);
|
|
65
|
+
const dashes = '─'.repeat(numDashes);
|
|
66
|
+
return `${indentation}└${dashes} ${colors.cyan(displayedName)}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Computes the actual column where the name starts on screen,
|
|
70
|
+
* accounting for MIN_DASHES potentially pushing the name further right.
|
|
71
|
+
*/
|
|
72
|
+
export function actualDisplayColumn(displayColumn, depth, baseIndent) {
|
|
73
|
+
if (depth === 0)
|
|
74
|
+
return displayColumn;
|
|
75
|
+
const numDashes = Math.max(MIN_DASHES, displayColumn - baseIndent - 2);
|
|
76
|
+
return baseIndent + numDashes + 2;
|
|
77
|
+
}
|
|
78
|
+
export function sharedDirPrefix(a, b) {
|
|
79
|
+
const aParts = a.split('/');
|
|
80
|
+
const bParts = b.split('/');
|
|
81
|
+
let shared = '';
|
|
82
|
+
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
|
|
83
|
+
if (aParts[i] === bParts[i]) {
|
|
84
|
+
shared += aParts[i] + '/';
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return shared;
|
|
91
|
+
}
|
|
92
|
+
export function globalPrefixFromPath(path, defaultControllersPath) {
|
|
93
|
+
if (path === defaultControllersPath)
|
|
94
|
+
return 'controllers/';
|
|
95
|
+
const normalized = path.replace(/\/$/, '');
|
|
96
|
+
const defaultNormalized = defaultControllersPath.replace(/\/$/, '');
|
|
97
|
+
if (normalized.startsWith(defaultNormalized + '/')) {
|
|
98
|
+
const suffix = normalized.slice(defaultNormalized.length + 1);
|
|
99
|
+
return `controllers/${suffix}/`;
|
|
100
|
+
}
|
|
101
|
+
return 'controllers/';
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Returns the directory portion of a globalName.
|
|
105
|
+
* e.g. "controllers/Admin/AuthedController" -> "controllers/Admin/"
|
|
106
|
+
* "controllers/ApplicationController" -> "controllers/"
|
|
107
|
+
*/
|
|
108
|
+
export function globalNameDir(globalName) {
|
|
109
|
+
const lastSlash = globalName.lastIndexOf('/');
|
|
110
|
+
if (lastSlash === -1)
|
|
111
|
+
return '';
|
|
112
|
+
return globalName.slice(0, lastSlash + 1);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns the parent directory of a directory path.
|
|
116
|
+
* e.g. "controllers/Admin/" -> "controllers/"
|
|
117
|
+
* "controllers/" -> ""
|
|
118
|
+
*/
|
|
119
|
+
export function parentOfDir(dir) {
|
|
120
|
+
const withoutTrailing = dir.slice(0, -1);
|
|
121
|
+
const lastSlash = withoutTrailing.lastIndexOf('/');
|
|
122
|
+
if (lastSlash === -1)
|
|
123
|
+
return '';
|
|
124
|
+
return withoutTrailing.slice(0, lastSlash + 1);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Checks whether a child controller violates the hierarchy convention.
|
|
128
|
+
* A controller should extend a controller in the same directory or the parent directory.
|
|
129
|
+
* Returns a warning message string if violated, or null if OK.
|
|
130
|
+
*/
|
|
131
|
+
export function hierarchyViolation(childGlobalName, parentGlobalName, controllersPath) {
|
|
132
|
+
const childDir = globalNameDir(childGlobalName);
|
|
133
|
+
const parentControllerDir = globalNameDir(parentGlobalName);
|
|
134
|
+
if (childDir === parentControllerDir || parentOfDir(childDir) === parentControllerDir) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const childFilePath = controllersPath + '/' + childGlobalName.replace(/^controllers\//, '') + '.ts';
|
|
138
|
+
return `[hierarchy violation: ${childFilePath} should extend a BaseController at its same level]`;
|
|
139
|
+
}
|
|
140
|
+
function collectTreeLines(controller, childrenMap, { depth, displayColumn, strippedPrefix, baseIndent, }, lines, controllersPath) {
|
|
141
|
+
const displayedName = controller.globalName.slice(strippedPrefix.length);
|
|
142
|
+
lines.push(controllerTreeLine(displayedName, depth, displayColumn, baseIndent));
|
|
143
|
+
// The actual column where the name appears on screen (may differ from displayColumn
|
|
144
|
+
// when MIN_DASHES pushes the name further right)
|
|
145
|
+
const effectiveDisplayColumn = actualDisplayColumn(displayColumn, depth, baseIndent);
|
|
146
|
+
if (depth > 0) {
|
|
147
|
+
const parentClass = Object.getPrototypeOf(controller);
|
|
148
|
+
if (parentClass !== PsychicController) {
|
|
149
|
+
const violation = hierarchyViolation(controller.globalName, parentClass.globalName, controllersPath);
|
|
150
|
+
if (violation) {
|
|
151
|
+
const warningIndent = ' '.repeat(effectiveDisplayColumn);
|
|
152
|
+
lines.push(warningIndent + colors.yellow(violation));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const childBaseIndent = depth === 0 ? ROOT_CHILD_INDENT : effectiveDisplayColumn - 1;
|
|
157
|
+
const children = childrenMap.get(controller) ?? [];
|
|
158
|
+
for (const child of children) {
|
|
159
|
+
const shared = sharedDirPrefix(controller.globalName, child.globalName);
|
|
160
|
+
const childDisplayColumn = effectiveDisplayColumn + shared.length - strippedPrefix.length;
|
|
161
|
+
collectTreeLines(child, childrenMap, {
|
|
162
|
+
depth: depth + 1,
|
|
163
|
+
displayColumn: childDisplayColumn,
|
|
164
|
+
strippedPrefix: shared,
|
|
165
|
+
baseIndent: childBaseIndent,
|
|
166
|
+
}, lines, controllersPath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -10,6 +10,7 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
10
10
|
import enumsFileStr from './helpers/enumsFileStr.js';
|
|
11
11
|
import generateRouteTypes from './helpers/generateRouteTypes.js';
|
|
12
12
|
import { OpenApiSpecDiff } from './helpers/OpenApiSpecDiff.js';
|
|
13
|
+
import printControllerHierarchy, { controllerHierarchyViolations, } from './helpers/printControllerHierarchy.js';
|
|
13
14
|
import printRoutes from './helpers/printRoutes.js';
|
|
14
15
|
export { BreakingChangesDetectedInOpenApiSpecError } from './helpers/OpenApiSpecDiff.js';
|
|
15
16
|
export default class PsychicBin {
|
|
@@ -26,6 +27,12 @@ export default class PsychicBin {
|
|
|
26
27
|
static printRoutes() {
|
|
27
28
|
printRoutes();
|
|
28
29
|
}
|
|
30
|
+
static printControllerHierarchy(controllersPath) {
|
|
31
|
+
printControllerHierarchy(controllersPath);
|
|
32
|
+
}
|
|
33
|
+
static controllerHierarchyViolations(controllersPath) {
|
|
34
|
+
return controllerHierarchyViolations(controllersPath);
|
|
35
|
+
}
|
|
29
36
|
static async sync({ bypassDreamSync = false, schemaOnly = false, } = {}) {
|
|
30
37
|
if (!bypassDreamSync)
|
|
31
38
|
await DreamBin.sync(() => { }, { schemaOnly });
|
|
@@ -160,6 +160,31 @@ export default class PsychicCLI {
|
|
|
160
160
|
await generateSyncOpenapiTypescriptInitializer(openapiFilepath, outfile, initializerName);
|
|
161
161
|
process.exit();
|
|
162
162
|
});
|
|
163
|
+
program
|
|
164
|
+
.command('inspect:controller-hierarchy')
|
|
165
|
+
.alias('i:controller-hierarchy')
|
|
166
|
+
.description('Displays the inheritance hierarchy of all PsychicController classes in the project.')
|
|
167
|
+
.argument('[path]', 'the controllers directory to scan (defaults to the configured controllers path)')
|
|
168
|
+
.action(async (controllersPath) => {
|
|
169
|
+
await initializePsychicApp();
|
|
170
|
+
PsychicBin.printControllerHierarchy(controllersPath);
|
|
171
|
+
process.exit();
|
|
172
|
+
});
|
|
173
|
+
program
|
|
174
|
+
.command('check:controller-hierarchy')
|
|
175
|
+
.description('Checks that all controllers extend a controller in the same or parent directory. Exits with an error if any violations are found.')
|
|
176
|
+
.argument('[path]', 'the controllers directory to scan (defaults to the configured controllers path)')
|
|
177
|
+
.action(async (controllersPath) => {
|
|
178
|
+
await initializePsychicApp();
|
|
179
|
+
const violations = PsychicBin.controllerHierarchyViolations(controllersPath);
|
|
180
|
+
if (violations.length > 0) {
|
|
181
|
+
for (const violation of violations) {
|
|
182
|
+
console.error(violation);
|
|
183
|
+
}
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
process.exit();
|
|
187
|
+
});
|
|
163
188
|
program
|
|
164
189
|
.command('routes')
|
|
165
190
|
.description('Prints a list of routes defined by the application, including path arguments and the controller/action reached by the route.')
|
|
@@ -197,7 +197,10 @@ export default class PsychicController {
|
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
|
-
* Gets the HTTP request headers from the
|
|
200
|
+
* Gets the HTTP request headers from the Koa ctx object.
|
|
201
|
+
* We recommend using the #header method instead when looking
|
|
202
|
+
* to retreive the value for a specific header, since that method
|
|
203
|
+
* will be safe with regards to case sensitivity.
|
|
201
204
|
*
|
|
202
205
|
* @returns The request headers as a key-value object where header names are lowercase strings
|
|
203
206
|
* and values can be strings, string arrays, or undefined.
|
|
@@ -216,6 +219,16 @@ export default class PsychicController {
|
|
|
216
219
|
get headers() {
|
|
217
220
|
return this.ctx.request.headers;
|
|
218
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* returns the value for the requested header. This method is case insensitive.
|
|
224
|
+
* If the header requested is not found, a blank string is returned.
|
|
225
|
+
*
|
|
226
|
+
* @param headerName - the name of the header
|
|
227
|
+
* @returns string
|
|
228
|
+
*/
|
|
229
|
+
header(headerName) {
|
|
230
|
+
return this.ctx.request.get(headerName);
|
|
231
|
+
}
|
|
219
232
|
/**
|
|
220
233
|
* Gets the combined parameters from the HTTP request. This includes URL parameters,
|
|
221
234
|
* request body, and query string parameters merged together. The merge order is:
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import colors from 'yoctocolors';
|
|
2
|
+
import PsychicController from '../../controller/index.js';
|
|
3
|
+
import PsychicApp from '../../psychic-app/index.js';
|
|
4
|
+
const ROOT_CHILD_INDENT = 5;
|
|
5
|
+
const MIN_DASHES = 2;
|
|
6
|
+
export default function printControllerHierarchy(controllersPath) {
|
|
7
|
+
const lines = controllerHierarchyLines(controllersPath);
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
console.log(line);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function resolveControllerClasses(controllersPath) {
|
|
13
|
+
const psychicApp = PsychicApp.getOrFail();
|
|
14
|
+
const resolvedPath = controllersPath ?? psychicApp.paths.controllers;
|
|
15
|
+
const allControllers = psychicApp.controllers;
|
|
16
|
+
const controllerClasses = Object.values(allControllers).filter(ctrl => {
|
|
17
|
+
return ctrl.globalName.startsWith(globalPrefixFromPath(resolvedPath, psychicApp.paths.controllers));
|
|
18
|
+
});
|
|
19
|
+
return { controllerClasses, resolvedPath };
|
|
20
|
+
}
|
|
21
|
+
export function controllerHierarchyLines(controllersPath) {
|
|
22
|
+
const { controllerClasses, resolvedPath } = resolveControllerClasses(controllersPath);
|
|
23
|
+
const childrenMap = buildChildrenMap(controllerClasses);
|
|
24
|
+
const roots = controllerClasses.filter(ctrl => !controllerClasses.some(other => other !== ctrl && ctrl.prototype instanceof other));
|
|
25
|
+
if (roots.length === 0) {
|
|
26
|
+
return ['No controllers found.'];
|
|
27
|
+
}
|
|
28
|
+
const lines = [];
|
|
29
|
+
for (const root of roots) {
|
|
30
|
+
collectTreeLines(root, childrenMap, { depth: 0, displayColumn: 0, strippedPrefix: '', baseIndent: 0 }, lines, resolvedPath);
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
export function controllerHierarchyViolations(controllersPath) {
|
|
35
|
+
const { controllerClasses, resolvedPath } = resolveControllerClasses(controllersPath);
|
|
36
|
+
const violations = [];
|
|
37
|
+
for (const ctrl of controllerClasses) {
|
|
38
|
+
const parentClass = Object.getPrototypeOf(ctrl);
|
|
39
|
+
if (parentClass === PsychicController)
|
|
40
|
+
continue;
|
|
41
|
+
const violation = hierarchyViolation(ctrl.globalName, parentClass.globalName, resolvedPath);
|
|
42
|
+
if (violation) {
|
|
43
|
+
violations.push(violation);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return violations;
|
|
47
|
+
}
|
|
48
|
+
export function buildChildrenMap(controllerClasses) {
|
|
49
|
+
const childrenMap = new Map();
|
|
50
|
+
for (const ctrl of controllerClasses) {
|
|
51
|
+
const parent = Object.getPrototypeOf(ctrl);
|
|
52
|
+
if (!childrenMap.has(parent)) {
|
|
53
|
+
childrenMap.set(parent, []);
|
|
54
|
+
}
|
|
55
|
+
childrenMap.get(parent).push(ctrl);
|
|
56
|
+
}
|
|
57
|
+
return childrenMap;
|
|
58
|
+
}
|
|
59
|
+
export function controllerTreeLine(displayedName, depth, displayColumn, baseIndent) {
|
|
60
|
+
if (depth === 0) {
|
|
61
|
+
return colors.cyan(displayedName);
|
|
62
|
+
}
|
|
63
|
+
const numDashes = Math.max(MIN_DASHES, displayColumn - baseIndent - 2);
|
|
64
|
+
const indentation = ' '.repeat(baseIndent);
|
|
65
|
+
const dashes = '─'.repeat(numDashes);
|
|
66
|
+
return `${indentation}└${dashes} ${colors.cyan(displayedName)}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Computes the actual column where the name starts on screen,
|
|
70
|
+
* accounting for MIN_DASHES potentially pushing the name further right.
|
|
71
|
+
*/
|
|
72
|
+
export function actualDisplayColumn(displayColumn, depth, baseIndent) {
|
|
73
|
+
if (depth === 0)
|
|
74
|
+
return displayColumn;
|
|
75
|
+
const numDashes = Math.max(MIN_DASHES, displayColumn - baseIndent - 2);
|
|
76
|
+
return baseIndent + numDashes + 2;
|
|
77
|
+
}
|
|
78
|
+
export function sharedDirPrefix(a, b) {
|
|
79
|
+
const aParts = a.split('/');
|
|
80
|
+
const bParts = b.split('/');
|
|
81
|
+
let shared = '';
|
|
82
|
+
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
|
|
83
|
+
if (aParts[i] === bParts[i]) {
|
|
84
|
+
shared += aParts[i] + '/';
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return shared;
|
|
91
|
+
}
|
|
92
|
+
export function globalPrefixFromPath(path, defaultControllersPath) {
|
|
93
|
+
if (path === defaultControllersPath)
|
|
94
|
+
return 'controllers/';
|
|
95
|
+
const normalized = path.replace(/\/$/, '');
|
|
96
|
+
const defaultNormalized = defaultControllersPath.replace(/\/$/, '');
|
|
97
|
+
if (normalized.startsWith(defaultNormalized + '/')) {
|
|
98
|
+
const suffix = normalized.slice(defaultNormalized.length + 1);
|
|
99
|
+
return `controllers/${suffix}/`;
|
|
100
|
+
}
|
|
101
|
+
return 'controllers/';
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Returns the directory portion of a globalName.
|
|
105
|
+
* e.g. "controllers/Admin/AuthedController" -> "controllers/Admin/"
|
|
106
|
+
* "controllers/ApplicationController" -> "controllers/"
|
|
107
|
+
*/
|
|
108
|
+
export function globalNameDir(globalName) {
|
|
109
|
+
const lastSlash = globalName.lastIndexOf('/');
|
|
110
|
+
if (lastSlash === -1)
|
|
111
|
+
return '';
|
|
112
|
+
return globalName.slice(0, lastSlash + 1);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns the parent directory of a directory path.
|
|
116
|
+
* e.g. "controllers/Admin/" -> "controllers/"
|
|
117
|
+
* "controllers/" -> ""
|
|
118
|
+
*/
|
|
119
|
+
export function parentOfDir(dir) {
|
|
120
|
+
const withoutTrailing = dir.slice(0, -1);
|
|
121
|
+
const lastSlash = withoutTrailing.lastIndexOf('/');
|
|
122
|
+
if (lastSlash === -1)
|
|
123
|
+
return '';
|
|
124
|
+
return withoutTrailing.slice(0, lastSlash + 1);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Checks whether a child controller violates the hierarchy convention.
|
|
128
|
+
* A controller should extend a controller in the same directory or the parent directory.
|
|
129
|
+
* Returns a warning message string if violated, or null if OK.
|
|
130
|
+
*/
|
|
131
|
+
export function hierarchyViolation(childGlobalName, parentGlobalName, controllersPath) {
|
|
132
|
+
const childDir = globalNameDir(childGlobalName);
|
|
133
|
+
const parentControllerDir = globalNameDir(parentGlobalName);
|
|
134
|
+
if (childDir === parentControllerDir || parentOfDir(childDir) === parentControllerDir) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const childFilePath = controllersPath + '/' + childGlobalName.replace(/^controllers\//, '') + '.ts';
|
|
138
|
+
return `[hierarchy violation: ${childFilePath} should extend a BaseController at its same level]`;
|
|
139
|
+
}
|
|
140
|
+
function collectTreeLines(controller, childrenMap, { depth, displayColumn, strippedPrefix, baseIndent, }, lines, controllersPath) {
|
|
141
|
+
const displayedName = controller.globalName.slice(strippedPrefix.length);
|
|
142
|
+
lines.push(controllerTreeLine(displayedName, depth, displayColumn, baseIndent));
|
|
143
|
+
// The actual column where the name appears on screen (may differ from displayColumn
|
|
144
|
+
// when MIN_DASHES pushes the name further right)
|
|
145
|
+
const effectiveDisplayColumn = actualDisplayColumn(displayColumn, depth, baseIndent);
|
|
146
|
+
if (depth > 0) {
|
|
147
|
+
const parentClass = Object.getPrototypeOf(controller);
|
|
148
|
+
if (parentClass !== PsychicController) {
|
|
149
|
+
const violation = hierarchyViolation(controller.globalName, parentClass.globalName, controllersPath);
|
|
150
|
+
if (violation) {
|
|
151
|
+
const warningIndent = ' '.repeat(effectiveDisplayColumn);
|
|
152
|
+
lines.push(warningIndent + colors.yellow(violation));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const childBaseIndent = depth === 0 ? ROOT_CHILD_INDENT : effectiveDisplayColumn - 1;
|
|
157
|
+
const children = childrenMap.get(controller) ?? [];
|
|
158
|
+
for (const child of children) {
|
|
159
|
+
const shared = sharedDirPrefix(controller.globalName, child.globalName);
|
|
160
|
+
const childDisplayColumn = effectiveDisplayColumn + shared.length - strippedPrefix.length;
|
|
161
|
+
collectTreeLines(child, childrenMap, {
|
|
162
|
+
depth: depth + 1,
|
|
163
|
+
displayColumn: childDisplayColumn,
|
|
164
|
+
strippedPrefix: shared,
|
|
165
|
+
baseIndent: childBaseIndent,
|
|
166
|
+
}, lines, controllersPath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -10,6 +10,7 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
10
10
|
import enumsFileStr from './helpers/enumsFileStr.js';
|
|
11
11
|
import generateRouteTypes from './helpers/generateRouteTypes.js';
|
|
12
12
|
import { OpenApiSpecDiff } from './helpers/OpenApiSpecDiff.js';
|
|
13
|
+
import printControllerHierarchy, { controllerHierarchyViolations, } from './helpers/printControllerHierarchy.js';
|
|
13
14
|
import printRoutes from './helpers/printRoutes.js';
|
|
14
15
|
export { BreakingChangesDetectedInOpenApiSpecError } from './helpers/OpenApiSpecDiff.js';
|
|
15
16
|
export default class PsychicBin {
|
|
@@ -26,6 +27,12 @@ export default class PsychicBin {
|
|
|
26
27
|
static printRoutes() {
|
|
27
28
|
printRoutes();
|
|
28
29
|
}
|
|
30
|
+
static printControllerHierarchy(controllersPath) {
|
|
31
|
+
printControllerHierarchy(controllersPath);
|
|
32
|
+
}
|
|
33
|
+
static controllerHierarchyViolations(controllersPath) {
|
|
34
|
+
return controllerHierarchyViolations(controllersPath);
|
|
35
|
+
}
|
|
29
36
|
static async sync({ bypassDreamSync = false, schemaOnly = false, } = {}) {
|
|
30
37
|
if (!bypassDreamSync)
|
|
31
38
|
await DreamBin.sync(() => { }, { schemaOnly });
|
|
@@ -160,6 +160,31 @@ export default class PsychicCLI {
|
|
|
160
160
|
await generateSyncOpenapiTypescriptInitializer(openapiFilepath, outfile, initializerName);
|
|
161
161
|
process.exit();
|
|
162
162
|
});
|
|
163
|
+
program
|
|
164
|
+
.command('inspect:controller-hierarchy')
|
|
165
|
+
.alias('i:controller-hierarchy')
|
|
166
|
+
.description('Displays the inheritance hierarchy of all PsychicController classes in the project.')
|
|
167
|
+
.argument('[path]', 'the controllers directory to scan (defaults to the configured controllers path)')
|
|
168
|
+
.action(async (controllersPath) => {
|
|
169
|
+
await initializePsychicApp();
|
|
170
|
+
PsychicBin.printControllerHierarchy(controllersPath);
|
|
171
|
+
process.exit();
|
|
172
|
+
});
|
|
173
|
+
program
|
|
174
|
+
.command('check:controller-hierarchy')
|
|
175
|
+
.description('Checks that all controllers extend a controller in the same or parent directory. Exits with an error if any violations are found.')
|
|
176
|
+
.argument('[path]', 'the controllers directory to scan (defaults to the configured controllers path)')
|
|
177
|
+
.action(async (controllersPath) => {
|
|
178
|
+
await initializePsychicApp();
|
|
179
|
+
const violations = PsychicBin.controllerHierarchyViolations(controllersPath);
|
|
180
|
+
if (violations.length > 0) {
|
|
181
|
+
for (const violation of violations) {
|
|
182
|
+
console.error(violation);
|
|
183
|
+
}
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
process.exit();
|
|
187
|
+
});
|
|
163
188
|
program
|
|
164
189
|
.command('routes')
|
|
165
190
|
.description('Prints a list of routes defined by the application, including path arguments and the controller/action reached by the route.')
|
|
@@ -197,7 +197,10 @@ export default class PsychicController {
|
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
|
-
* Gets the HTTP request headers from the
|
|
200
|
+
* Gets the HTTP request headers from the Koa ctx object.
|
|
201
|
+
* We recommend using the #header method instead when looking
|
|
202
|
+
* to retreive the value for a specific header, since that method
|
|
203
|
+
* will be safe with regards to case sensitivity.
|
|
201
204
|
*
|
|
202
205
|
* @returns The request headers as a key-value object where header names are lowercase strings
|
|
203
206
|
* and values can be strings, string arrays, or undefined.
|
|
@@ -216,6 +219,16 @@ export default class PsychicController {
|
|
|
216
219
|
get headers() {
|
|
217
220
|
return this.ctx.request.headers;
|
|
218
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* returns the value for the requested header. This method is case insensitive.
|
|
224
|
+
* If the header requested is not found, a blank string is returned.
|
|
225
|
+
*
|
|
226
|
+
* @param headerName - the name of the header
|
|
227
|
+
* @returns string
|
|
228
|
+
*/
|
|
229
|
+
header(headerName) {
|
|
230
|
+
return this.ctx.request.get(headerName);
|
|
231
|
+
}
|
|
219
232
|
/**
|
|
220
233
|
* Gets the combined parameters from the HTTP request. This includes URL parameters,
|
|
221
234
|
* request body, and query string parameters merged together. The merge order is:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import PsychicController from '../../controller/index.js';
|
|
2
|
+
export default function printControllerHierarchy(controllersPath?: string): void;
|
|
3
|
+
export declare function resolveControllerClasses(controllersPath?: string): {
|
|
4
|
+
controllerClasses: (typeof PsychicController)[];
|
|
5
|
+
resolvedPath: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function controllerHierarchyLines(controllersPath?: string): string[];
|
|
8
|
+
export declare function controllerHierarchyViolations(controllersPath?: string): string[];
|
|
9
|
+
export declare function buildChildrenMap(controllerClasses: (typeof PsychicController)[]): Map<typeof PsychicController, (typeof PsychicController)[]>;
|
|
10
|
+
export declare function controllerTreeLine(displayedName: string, depth: number, displayColumn: number, baseIndent: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* Computes the actual column where the name starts on screen,
|
|
13
|
+
* accounting for MIN_DASHES potentially pushing the name further right.
|
|
14
|
+
*/
|
|
15
|
+
export declare function actualDisplayColumn(displayColumn: number, depth: number, baseIndent: number): number;
|
|
16
|
+
export declare function sharedDirPrefix(a: string, b: string): string;
|
|
17
|
+
export declare function globalPrefixFromPath(path: string, defaultControllersPath: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Returns the directory portion of a globalName.
|
|
20
|
+
* e.g. "controllers/Admin/AuthedController" -> "controllers/Admin/"
|
|
21
|
+
* "controllers/ApplicationController" -> "controllers/"
|
|
22
|
+
*/
|
|
23
|
+
export declare function globalNameDir(globalName: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the parent directory of a directory path.
|
|
26
|
+
* e.g. "controllers/Admin/" -> "controllers/"
|
|
27
|
+
* "controllers/" -> ""
|
|
28
|
+
*/
|
|
29
|
+
export declare function parentOfDir(dir: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Checks whether a child controller violates the hierarchy convention.
|
|
32
|
+
* A controller should extend a controller in the same directory or the parent directory.
|
|
33
|
+
* Returns a warning message string if violated, or null if OK.
|
|
34
|
+
*/
|
|
35
|
+
export declare function hierarchyViolation(childGlobalName: string, parentGlobalName: string, controllersPath: string): string | null;
|
|
@@ -10,6 +10,8 @@ export default class PsychicBin {
|
|
|
10
10
|
modelName?: string;
|
|
11
11
|
}): Promise<void>;
|
|
12
12
|
static printRoutes(): void;
|
|
13
|
+
static printControllerHierarchy(controllersPath?: string): void;
|
|
14
|
+
static controllerHierarchyViolations(controllersPath?: string): string[];
|
|
13
15
|
static sync({ bypassDreamSync, schemaOnly, }?: {
|
|
14
16
|
bypassDreamSync?: boolean;
|
|
15
17
|
schemaOnly?: boolean;
|
|
@@ -111,7 +111,10 @@ export default class PsychicController {
|
|
|
111
111
|
action: string;
|
|
112
112
|
});
|
|
113
113
|
/**
|
|
114
|
-
* Gets the HTTP request headers from the
|
|
114
|
+
* Gets the HTTP request headers from the Koa ctx object.
|
|
115
|
+
* We recommend using the #header method instead when looking
|
|
116
|
+
* to retreive the value for a specific header, since that method
|
|
117
|
+
* will be safe with regards to case sensitivity.
|
|
115
118
|
*
|
|
116
119
|
* @returns The request headers as a key-value object where header names are lowercase strings
|
|
117
120
|
* and values can be strings, string arrays, or undefined.
|
|
@@ -128,6 +131,14 @@ export default class PsychicController {
|
|
|
128
131
|
* ```
|
|
129
132
|
*/
|
|
130
133
|
get headers(): import("http").IncomingHttpHeaders;
|
|
134
|
+
/**
|
|
135
|
+
* returns the value for the requested header. This method is case insensitive.
|
|
136
|
+
* If the header requested is not found, a blank string is returned.
|
|
137
|
+
*
|
|
138
|
+
* @param headerName - the name of the header
|
|
139
|
+
* @returns string
|
|
140
|
+
*/
|
|
141
|
+
header(headerName: string): string;
|
|
131
142
|
/**
|
|
132
143
|
* Gets the combined parameters from the HTTP request. This includes URL parameters,
|
|
133
144
|
* request body, and query string parameters merged together. The merge order is:
|