@adaptivestone/framework 3.0.17 → 3.0.19
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/CHANGELOG.md +13 -0
- package/commands/Documentation.js +1 -2
- package/commands/GetOpenApiJson.js +223 -0
- package/modules/AbstractController.js +54 -31
- package/package.json +8 -7
- package/services/http/middleware/AbstractMiddleware.js +5 -1
- package/services/http/middleware/GetUserByToken.js +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
### 3.1.0 // NEXT
|
|
2
|
+
|
|
3
|
+
[NEW] new comand to generate open API documentation (wip)
|
|
4
|
+
|
|
5
|
+
### 3.0.19
|
|
6
|
+
|
|
7
|
+
[UPDATE] update deps
|
|
8
|
+
|
|
9
|
+
### 3.0.18
|
|
10
|
+
|
|
11
|
+
[UPDATE] update deps
|
|
12
|
+
[UPDATE] change default branch to 'main'
|
|
13
|
+
|
|
1
14
|
### 3.0.17
|
|
2
15
|
|
|
3
16
|
[UPDATE] update deps
|
|
@@ -6,8 +6,7 @@ class Documentation extends AbstractCommand {
|
|
|
6
6
|
const CM = new ControllerManager(this.app);
|
|
7
7
|
this.app.documentation = [];
|
|
8
8
|
await CM.initControllers({ folders: this.app.foldersConfig });
|
|
9
|
-
|
|
10
|
-
return JSON.stringify(this.app.documentation);
|
|
9
|
+
return this.app.documentation;
|
|
11
10
|
}
|
|
12
11
|
}
|
|
13
12
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const AbstractCommand = require('../modules/AbstractCommand');
|
|
2
|
+
|
|
3
|
+
class GetOpenApiJson extends AbstractCommand {
|
|
4
|
+
async run() {
|
|
5
|
+
const { myDomain } = this.app.getConfig('http');
|
|
6
|
+
const openApi = {
|
|
7
|
+
openapi: '3.0.0',
|
|
8
|
+
info: {
|
|
9
|
+
title: 'Some title',
|
|
10
|
+
description: 'This is a simple API',
|
|
11
|
+
contact: {
|
|
12
|
+
email: 'you@your-company.com',
|
|
13
|
+
},
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
},
|
|
16
|
+
servers: [
|
|
17
|
+
{
|
|
18
|
+
url: 'http://localhost:3300',
|
|
19
|
+
description: 'Localhost',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
url: myDomain,
|
|
23
|
+
description: 'Domain from config',
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const baseDocumentation = await this.app.runCliCommand('documentation');
|
|
29
|
+
|
|
30
|
+
if (!baseDocumentation) {
|
|
31
|
+
throw new Error('Problems with basic documenation generation');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
openApi.components = {};
|
|
35
|
+
openApi.components.securitySchemes = {};
|
|
36
|
+
|
|
37
|
+
openApi.tags = [];
|
|
38
|
+
|
|
39
|
+
for (const controller of baseDocumentation) {
|
|
40
|
+
const controllerName = controller.contollerName.split('/')[0];
|
|
41
|
+
if (!openApi.tags.find((tag) => tag.name === controllerName)) {
|
|
42
|
+
openApi.tags.push({
|
|
43
|
+
name: controllerName,
|
|
44
|
+
description: '',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
openApi.paths = {};
|
|
50
|
+
|
|
51
|
+
for (const controller of baseDocumentation) {
|
|
52
|
+
for (const route of controller.routesInfo) {
|
|
53
|
+
const routeInfo = route[Object.keys(route)?.[0]];
|
|
54
|
+
const middlewares = [
|
|
55
|
+
...routeInfo.controllerMiddlewares,
|
|
56
|
+
...routeInfo.routeMiddlewares,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const securitySchemaNames = [];
|
|
60
|
+
|
|
61
|
+
if (middlewares?.length) {
|
|
62
|
+
for (const middleware of middlewares) {
|
|
63
|
+
if (middleware?.authParams?.length) {
|
|
64
|
+
for (const authParam of middleware.authParams) {
|
|
65
|
+
if (!openApi.components.securitySchemes[authParam.name]) {
|
|
66
|
+
openApi.components.securitySchemes[authParam.name] =
|
|
67
|
+
authParam;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
securitySchemaNames.push({
|
|
71
|
+
[authParam.name]: [],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let routeName = Object.keys(route)[0];
|
|
79
|
+
|
|
80
|
+
if (routeName === '/') {
|
|
81
|
+
// eslint-disable-next-line no-continue
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (routeName.slice(-1) === '/') {
|
|
86
|
+
routeName = routeName.substring(0, routeName.length - 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const partsRoute = routeName.split('/');
|
|
90
|
+
|
|
91
|
+
const newRoute = [];
|
|
92
|
+
const routeParameters = [];
|
|
93
|
+
for (const routeDetail of partsRoute) {
|
|
94
|
+
let routeCopy = routeDetail;
|
|
95
|
+
|
|
96
|
+
if (routeDetail.startsWith(':')) {
|
|
97
|
+
const routeChange = routeCopy.split('');
|
|
98
|
+
routeChange[0] = '{';
|
|
99
|
+
routeChange.push('}');
|
|
100
|
+
routeCopy = routeChange.join('');
|
|
101
|
+
routeParameters.push(routeCopy.replace(/^.|.$/g, ''));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
newRoute.push(routeCopy);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
routeName = newRoute.join('/');
|
|
108
|
+
|
|
109
|
+
if (!openApi.paths[routeName]) {
|
|
110
|
+
openApi.paths[routeName] = {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const methodName = route[Object.keys(route)[0]].method.toLowerCase();
|
|
114
|
+
const routeTitle = route[Object.keys(route)[0]].name;
|
|
115
|
+
const routeDescription =
|
|
116
|
+
route[Object.keys(route)[0]]?.description || 'empty description';
|
|
117
|
+
const routeFields = route[Object.keys(route)[0]].fields;
|
|
118
|
+
|
|
119
|
+
if (!openApi.paths[routeName][methodName]) {
|
|
120
|
+
openApi.paths[routeName][methodName] = {};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
openApi.paths[routeName][methodName].tags = [];
|
|
124
|
+
|
|
125
|
+
openApi.paths[routeName][methodName].tags.push(
|
|
126
|
+
controller.contollerName.split('/')[0],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
openApi.paths[routeName][methodName].summary = routeTitle;
|
|
130
|
+
openApi.paths[routeName][methodName].description = routeDescription;
|
|
131
|
+
openApi.paths[routeName][methodName].parameters = [];
|
|
132
|
+
openApi.paths[routeName][methodName].security = securitySchemaNames;
|
|
133
|
+
|
|
134
|
+
openApi.paths[routeName][methodName].responses = {
|
|
135
|
+
200: {
|
|
136
|
+
description: 'Successfully',
|
|
137
|
+
},
|
|
138
|
+
201: {
|
|
139
|
+
description: 'The resource was created successfully',
|
|
140
|
+
},
|
|
141
|
+
400: {
|
|
142
|
+
description: 'There is a syntax error in the request',
|
|
143
|
+
},
|
|
144
|
+
401: {
|
|
145
|
+
description:
|
|
146
|
+
'Authentication is required to access the requested resource',
|
|
147
|
+
},
|
|
148
|
+
404: {
|
|
149
|
+
description:
|
|
150
|
+
'The server accepted the request, but did not find the corresponding resource at the specified URI',
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
for (const routeField of routeParameters) {
|
|
155
|
+
openApi.paths[routeName][methodName].parameters.push({
|
|
156
|
+
name: routeField,
|
|
157
|
+
in: 'path',
|
|
158
|
+
required: true,
|
|
159
|
+
schema: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (routeFields.length) {
|
|
166
|
+
const groupBodyFields = {};
|
|
167
|
+
const requiredFields = [];
|
|
168
|
+
for (const field of routeFields) {
|
|
169
|
+
if (field.isRequired) {
|
|
170
|
+
requiredFields.push(field.name);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (field.type === 'object') {
|
|
174
|
+
groupBodyFields[field.name] = {};
|
|
175
|
+
const objectFields = {};
|
|
176
|
+
for (const objField of field.fields) {
|
|
177
|
+
objectFields[objField.name] = {
|
|
178
|
+
// fields file has mixed type but openApi doesnt have this type
|
|
179
|
+
type: objField.type === 'mixed' ? 'string' : objField.type,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
groupBodyFields[field.name].properties = objectFields;
|
|
184
|
+
} else {
|
|
185
|
+
groupBodyFields[field.name] = {
|
|
186
|
+
type: field.type,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (field.type === 'array') {
|
|
190
|
+
groupBodyFields[field.name].items = {
|
|
191
|
+
type: field.innerType,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
openApi.paths[routeName][methodName].requestBody = {
|
|
198
|
+
content: {
|
|
199
|
+
'application/json': {
|
|
200
|
+
schema: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: groupBodyFields,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (requiredFields.length) {
|
|
209
|
+
openApi.paths[routeName][methodName].requestBody.content[
|
|
210
|
+
'application/json'
|
|
211
|
+
].schema.required = requiredFields;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result = JSON.stringify(openApi);
|
|
218
|
+
console.log(result);
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = GetOpenApiJson;
|
|
@@ -91,12 +91,14 @@ class AbstractController extends Base {
|
|
|
91
91
|
if (Array.isArray(M)) {
|
|
92
92
|
[MiddlewareFunction, middlewareParams] = M;
|
|
93
93
|
}
|
|
94
|
+
|
|
94
95
|
middlewaresInfo.push({
|
|
95
96
|
name: MiddlewareFunction.name,
|
|
96
97
|
method,
|
|
97
98
|
path: realPath,
|
|
98
99
|
fullPath,
|
|
99
100
|
params: middlewareParams,
|
|
101
|
+
authParams: MiddlewareFunction?.usedAuthParameters,
|
|
100
102
|
MiddlewareFunction,
|
|
101
103
|
});
|
|
102
104
|
}
|
|
@@ -105,10 +107,11 @@ class AbstractController extends Base {
|
|
|
105
107
|
};
|
|
106
108
|
|
|
107
109
|
const routeMiddlewaresReg = parseMiddlewares(routeMiddlewares);
|
|
108
|
-
const middlewaresInfo = parseMiddlewares(this.constructor.middleware);
|
|
109
110
|
|
|
111
|
+
const middlewaresInfo = parseMiddlewares(this.constructor.middleware);
|
|
110
112
|
const routesInfo = [];
|
|
111
113
|
let routeObjectClone = {};
|
|
114
|
+
const routeObjests = [];
|
|
112
115
|
|
|
113
116
|
/**
|
|
114
117
|
* Register controller middleware
|
|
@@ -140,7 +143,6 @@ class AbstractController extends Base {
|
|
|
140
143
|
middleware.path === path && middleware.method === verb,
|
|
141
144
|
);
|
|
142
145
|
let routeObject = routes[verb][path];
|
|
143
|
-
routeObjectClone = merge({}, routeObject);
|
|
144
146
|
if (Object.prototype.toString.call(routeObject) !== '[object Object]') {
|
|
145
147
|
routeObject = {
|
|
146
148
|
handler: routeObject,
|
|
@@ -172,7 +174,9 @@ class AbstractController extends Base {
|
|
|
172
174
|
|
|
173
175
|
routesInfo.push({
|
|
174
176
|
name: fnName,
|
|
177
|
+
description: routeObject?.description,
|
|
175
178
|
method: verb.toUpperCase(),
|
|
179
|
+
fields: routeObject?.request?.fields,
|
|
176
180
|
path,
|
|
177
181
|
fullPath,
|
|
178
182
|
});
|
|
@@ -268,6 +272,8 @@ class AbstractController extends Base {
|
|
|
268
272
|
});
|
|
269
273
|
},
|
|
270
274
|
);
|
|
275
|
+
|
|
276
|
+
routeObjectClone = merge({}, routeObject);
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
|
|
@@ -299,52 +305,63 @@ class AbstractController extends Base {
|
|
|
299
305
|
/**
|
|
300
306
|
* Generate documentation
|
|
301
307
|
*/
|
|
302
|
-
|
|
308
|
+
|
|
309
|
+
const processingFields = (fieldsByRoute) => {
|
|
303
310
|
const fields = [];
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
if (!fieldsByRoute) {
|
|
312
|
+
return fields;
|
|
313
|
+
}
|
|
314
|
+
const entries = Object.entries(fieldsByRoute);
|
|
315
|
+
entries.forEach(([key, value]) => {
|
|
316
|
+
const field = {};
|
|
317
|
+
field.name = key;
|
|
318
|
+
field.type = value.type;
|
|
319
|
+
if (value.exclusiveTests) {
|
|
320
|
+
field.isRequired = value.exclusiveTests.required;
|
|
321
|
+
}
|
|
322
|
+
if (value?.innerType) {
|
|
323
|
+
field.innerType = value?.innerType?.type;
|
|
324
|
+
}
|
|
314
325
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
});
|
|
326
|
+
if (value.fields) {
|
|
327
|
+
field.fields = [];
|
|
328
|
+
// eslint-disable-next-line no-shadow
|
|
329
|
+
const entries = Object.entries(value.fields);
|
|
330
|
+
// eslint-disable-next-line no-shadow
|
|
331
|
+
entries.forEach(([key, value]) => {
|
|
332
|
+
field.fields.push({
|
|
333
|
+
name: key,
|
|
334
|
+
type: value.type,
|
|
325
335
|
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
fields.push(field);
|
|
339
|
+
});
|
|
340
|
+
return fields;
|
|
341
|
+
};
|
|
330
342
|
|
|
343
|
+
if (!this.app.httpServer) {
|
|
331
344
|
this.app.documentation.push({
|
|
332
345
|
contollerName: this.getConstructorName(),
|
|
333
346
|
routesInfo: routesInfo.map((route) => ({
|
|
334
347
|
[route.fullPath]: {
|
|
335
348
|
method: route.method,
|
|
336
349
|
name: route.name,
|
|
337
|
-
|
|
350
|
+
description: route?.description,
|
|
351
|
+
fields: processingFields(route.fields),
|
|
338
352
|
routeMiddlewares: routeMiddlewaresReg
|
|
339
353
|
// eslint-disable-next-line consistent-return
|
|
340
354
|
.map((middleware) => {
|
|
341
355
|
if (
|
|
342
356
|
route.fullPath.toUpperCase() ===
|
|
343
|
-
|
|
357
|
+
middleware.fullPath.toUpperCase() ||
|
|
358
|
+
middleware.fullPath.toUpperCase() ===
|
|
359
|
+
`${route.fullPath.toUpperCase()}*`
|
|
344
360
|
) {
|
|
345
361
|
return {
|
|
346
362
|
name: middleware.name,
|
|
347
363
|
params: middleware.params,
|
|
364
|
+
authParams: middleware.authParams,
|
|
348
365
|
};
|
|
349
366
|
}
|
|
350
367
|
})
|
|
@@ -355,9 +372,15 @@ class AbstractController extends Base {
|
|
|
355
372
|
.filter(
|
|
356
373
|
(middleware) =>
|
|
357
374
|
middleware.fullPath.toUpperCase() ===
|
|
358
|
-
|
|
375
|
+
route.fullPath.toUpperCase() ||
|
|
376
|
+
middleware.fullPath.toUpperCase() ===
|
|
377
|
+
`${route.fullPath.toUpperCase()}*`,
|
|
359
378
|
)
|
|
360
|
-
.map(({ name, params }) => ({
|
|
379
|
+
.map(({ name, params, authParams }) => ({
|
|
380
|
+
name,
|
|
381
|
+
params,
|
|
382
|
+
authParams,
|
|
383
|
+
})),
|
|
361
384
|
),
|
|
362
385
|
],
|
|
363
386
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adaptivestone/framework",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.19",
|
|
4
4
|
"description": "Adaptive stone node js framework",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"engines": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"prod": "nodemon ./cluster.js",
|
|
17
17
|
"test": "jest",
|
|
18
18
|
"codestyle": "prettier --check '**/*.(js|jsx|ts|tsx|json|css|scss|md)'",
|
|
19
|
-
"prepare": "husky install"
|
|
19
|
+
"prepare": "husky install",
|
|
20
|
+
"cli": "node cliCommand"
|
|
20
21
|
},
|
|
21
22
|
"jest": {
|
|
22
23
|
"setupFilesAfterEnv": [
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"cors": "^2.8.5",
|
|
33
34
|
"deepmerge": "^4.2.2",
|
|
34
35
|
"dotenv": "^16.0.0",
|
|
35
|
-
"email-templates": "^
|
|
36
|
+
"email-templates": "^10.0.0",
|
|
36
37
|
"express": "^4.17.1",
|
|
37
38
|
"i18next": "^21.2.4",
|
|
38
39
|
"i18next-chained-backend": "^3.0.2",
|
|
@@ -56,10 +57,10 @@
|
|
|
56
57
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
57
58
|
"eslint-config-prettier": "^8.3.0",
|
|
58
59
|
"eslint-plugin-import": "^2.23.4",
|
|
59
|
-
"eslint-plugin-jest": "^
|
|
60
|
-
"husky": "^
|
|
61
|
-
"jest": "^
|
|
62
|
-
"lint-staged": "^
|
|
60
|
+
"eslint-plugin-jest": "^27.0.0",
|
|
61
|
+
"husky": "^8.0.0",
|
|
62
|
+
"jest": "^29.0.0",
|
|
63
|
+
"lint-staged": "^13.0.0",
|
|
63
64
|
"mongodb-memory-server": "^8.0.2",
|
|
64
65
|
"nodemon": "^2.0.12",
|
|
65
66
|
"supertest": "^6.1.4"
|
|
@@ -7,7 +7,11 @@ class AbstractMiddleware extends Base {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
static get description() {
|
|
10
|
-
return '
|
|
10
|
+
return 'Middleware description. Please provide own';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static get usedAuthParameters() {
|
|
14
|
+
return [];
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
async middleware(req, res, next) {
|
|
@@ -5,6 +5,17 @@ class GetUserByToken extends AbstractMiddleware {
|
|
|
5
5
|
return 'Grab a token and try to parse the user from it. It user exist will add req.appInfo.user variable';
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
static get usedAuthParameters() {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
name: 'Authorization',
|
|
12
|
+
type: 'apiKey',
|
|
13
|
+
in: 'header',
|
|
14
|
+
description: this?.description,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
async middleware(req, res, next) {
|
|
9
20
|
let { token } = req.body;
|
|
10
21
|
this.logger.verbose(
|