@adaptivestone/framework 3.0.18 → 3.0.20

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 CHANGED
@@ -1,3 +1,15 @@
1
+ ### 3.1.0 // NEXT
2
+
3
+ [NEW] new comand to generate open API documentation (wip)
4
+
5
+ ### 3.0.20
6
+
7
+ [UPDATE] update deps
8
+
9
+ ### 3.0.19
10
+
11
+ [UPDATE] update deps
12
+
1
13
  ### 3.0.18
2
14
 
3
15
  [UPDATE] update deps
@@ -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,
@@ -174,6 +176,7 @@ class AbstractController extends Base {
174
176
  name: fnName,
175
177
  description: routeObject?.description,
176
178
  method: verb.toUpperCase(),
179
+ fields: routeObject?.request?.fields,
177
180
  path,
178
181
  fullPath,
179
182
  });
@@ -250,6 +253,14 @@ class AbstractController extends Base {
250
253
  },
251
254
  });
252
255
 
256
+ if (!routeObject.handler) {
257
+ this.logger.error(`Route object have no handler defined`);
258
+ return res.status(500).json({
259
+ message:
260
+ 'Platform error 2. Please check later or contact support',
261
+ });
262
+ }
263
+
253
264
  if (routeObject.handler.constructor.name !== 'AsyncFunction') {
254
265
  const error =
255
266
  "Handler should be AsyncFunction. Perhabs you miss 'async' of function declaration?";
@@ -269,6 +280,8 @@ class AbstractController extends Base {
269
280
  });
270
281
  },
271
282
  );
283
+
284
+ routeObjectClone = merge({}, routeObject);
272
285
  }
273
286
  }
274
287
 
@@ -300,35 +313,42 @@ class AbstractController extends Base {
300
313
  /**
301
314
  * Generate documentation
302
315
  */
303
- if (!this.app.httpServer) {
316
+
317
+ const processingFields = (fieldsByRoute) => {
304
318
  const fields = [];
305
- if (routeObjectClone.request) {
306
- const reqFields = routeObjectClone.request.fields;
307
- const entries = Object.entries(reqFields);
308
- entries.forEach(([key, value]) => {
309
- const field = {};
310
- field.name = key;
311
- field.type = value.type;
312
- if (value.exclusiveTests) {
313
- field.isRequired = value.exclusiveTests.required;
314
- }
319
+ if (!fieldsByRoute) {
320
+ return fields;
321
+ }
322
+ const entries = Object.entries(fieldsByRoute);
323
+ entries.forEach(([key, value]) => {
324
+ const field = {};
325
+ field.name = key;
326
+ field.type = value.type;
327
+ if (value.exclusiveTests) {
328
+ field.isRequired = value.exclusiveTests.required;
329
+ }
330
+ if (value?.innerType) {
331
+ field.innerType = value?.innerType?.type;
332
+ }
315
333
 
316
- if (value.fields) {
317
- field.fields = [];
318
- // eslint-disable-next-line no-shadow
319
- const entries = Object.entries(value.fields);
320
- // eslint-disable-next-line no-shadow
321
- entries.forEach(([key, value]) => {
322
- field.fields.push({
323
- name: key,
324
- type: value.type,
325
- });
334
+ if (value.fields) {
335
+ field.fields = [];
336
+ // eslint-disable-next-line no-shadow
337
+ const entries = Object.entries(value.fields);
338
+ // eslint-disable-next-line no-shadow
339
+ entries.forEach(([key, value]) => {
340
+ field.fields.push({
341
+ name: key,
342
+ type: value.type,
326
343
  });
327
- }
328
- fields.push(field);
329
- });
330
- }
344
+ });
345
+ }
346
+ fields.push(field);
347
+ });
348
+ return fields;
349
+ };
331
350
 
351
+ if (!this.app.httpServer) {
332
352
  this.app.documentation.push({
333
353
  contollerName: this.getConstructorName(),
334
354
  routesInfo: routesInfo.map((route) => ({
@@ -336,17 +356,20 @@ class AbstractController extends Base {
336
356
  method: route.method,
337
357
  name: route.name,
338
358
  description: route?.description,
339
- fields,
359
+ fields: processingFields(route.fields),
340
360
  routeMiddlewares: routeMiddlewaresReg
341
361
  // eslint-disable-next-line consistent-return
342
362
  .map((middleware) => {
343
363
  if (
344
364
  route.fullPath.toUpperCase() ===
345
- middleware.fullPath.toUpperCase()
365
+ middleware.fullPath.toUpperCase() ||
366
+ middleware.fullPath.toUpperCase() ===
367
+ `${route.fullPath.toUpperCase()}*`
346
368
  ) {
347
369
  return {
348
370
  name: middleware.name,
349
371
  params: middleware.params,
372
+ authParams: middleware.authParams,
350
373
  };
351
374
  }
352
375
  })
@@ -357,9 +380,15 @@ class AbstractController extends Base {
357
380
  .filter(
358
381
  (middleware) =>
359
382
  middleware.fullPath.toUpperCase() ===
360
- route.fullPath.toUpperCase(),
383
+ route.fullPath.toUpperCase() ||
384
+ middleware.fullPath.toUpperCase() ===
385
+ `${route.fullPath.toUpperCase()}*`,
361
386
  )
362
- .map(({ name, params }) => ({ name, params })),
387
+ .map(({ name, params, authParams }) => ({
388
+ name,
389
+ params,
390
+ authParams,
391
+ })),
363
392
  ),
364
393
  ],
365
394
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptivestone/framework",
3
- "version": "3.0.18",
3
+ "version": "3.0.20",
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": [
@@ -56,9 +57,9 @@
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": "^26.0.0",
60
+ "eslint-plugin-jest": "^27.0.0",
60
61
  "husky": "^8.0.0",
61
- "jest": "^28.0.0",
62
+ "jest": "^29.0.0",
62
63
  "lint-staged": "^13.0.0",
63
64
  "mongodb-memory-server": "^8.0.2",
64
65
  "nodemon": "^2.0.12",
@@ -7,7 +7,11 @@ class AbstractMiddleware extends Base {
7
7
  }
8
8
 
9
9
  static get description() {
10
- return ' Middleware description. Please provide own';
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(
package/tests/setup.js CHANGED
@@ -15,7 +15,7 @@ beforeAll(async () => {
15
15
  await mongoMemoryServerInstance.waitUntilRunning();
16
16
  process.env.LOGGER_CONSOLE_LEVEL = 'error';
17
17
  const connectionStringMongo = await mongoMemoryServerInstance.getUri();
18
- console.info('MONGO_URI: ', connectionStringMongo);
18
+ // console.info('MONGO_URI: ', connectionStringMongo);
19
19
  global.server = new Server({
20
20
  folders: {
21
21
  config: process.env.TEST_FOLDER_CONFIG || path.resolve('./config'),