@akemona-org/strapi-plugin-documentation 3.7.0

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.
Files changed (64) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +170 -0
  3. package/admin/src/assets/images/logo.svg +1 -0
  4. package/admin/src/components/Block/components.js +26 -0
  5. package/admin/src/components/Block/index.js +39 -0
  6. package/admin/src/components/Row/ButtonContainer.js +53 -0
  7. package/admin/src/components/Row/components.js +83 -0
  8. package/admin/src/components/Row/index.js +57 -0
  9. package/admin/src/containers/App/index.js +30 -0
  10. package/admin/src/containers/HomePage/actions.js +72 -0
  11. package/admin/src/containers/HomePage/components.js +59 -0
  12. package/admin/src/containers/HomePage/constants.js +14 -0
  13. package/admin/src/containers/HomePage/index.js +283 -0
  14. package/admin/src/containers/HomePage/reducer.js +49 -0
  15. package/admin/src/containers/HomePage/saga.js +126 -0
  16. package/admin/src/containers/HomePage/selectors.js +34 -0
  17. package/admin/src/containers/Initializer/index.js +28 -0
  18. package/admin/src/containers/Initializer/tests/index.test.js +21 -0
  19. package/admin/src/index.js +57 -0
  20. package/admin/src/lifecycles.js +13 -0
  21. package/admin/src/permissions.js +19 -0
  22. package/admin/src/pluginId.js +5 -0
  23. package/admin/src/reducers.js +8 -0
  24. package/admin/src/translations/ar.json +23 -0
  25. package/admin/src/translations/cs.json +24 -0
  26. package/admin/src/translations/de.json +30 -0
  27. package/admin/src/translations/en.json +31 -0
  28. package/admin/src/translations/es.json +27 -0
  29. package/admin/src/translations/fr.json +29 -0
  30. package/admin/src/translations/id.json +28 -0
  31. package/admin/src/translations/index.js +49 -0
  32. package/admin/src/translations/it.json +29 -0
  33. package/admin/src/translations/ko.json +24 -0
  34. package/admin/src/translations/ms.json +26 -0
  35. package/admin/src/translations/nl.json +24 -0
  36. package/admin/src/translations/pl.json +27 -0
  37. package/admin/src/translations/pt-BR.json +24 -0
  38. package/admin/src/translations/pt.json +24 -0
  39. package/admin/src/translations/ru.json +31 -0
  40. package/admin/src/translations/sk.json +27 -0
  41. package/admin/src/translations/th.json +27 -0
  42. package/admin/src/translations/tr.json +23 -0
  43. package/admin/src/translations/uk.json +26 -0
  44. package/admin/src/translations/vi.json +27 -0
  45. package/admin/src/translations/zh-Hans.json +31 -0
  46. package/admin/src/translations/zh.json +27 -0
  47. package/admin/src/utils/getTrad.js +5 -0
  48. package/admin/src/utils/openWithNewTab.js +20 -0
  49. package/config/functions/bootstrap.js +138 -0
  50. package/config/policies/index.js +35 -0
  51. package/config/routes.json +74 -0
  52. package/config/settings.json +46 -0
  53. package/controllers/Documentation.js +303 -0
  54. package/middlewares/documentation/defaults.json +5 -0
  55. package/middlewares/documentation/index.js +59 -0
  56. package/package.json +89 -0
  57. package/public/index.html +57 -0
  58. package/public/login.html +135 -0
  59. package/services/Documentation.js +1861 -0
  60. package/services/Token.js +31 -0
  61. package/services/utils/components.json +25 -0
  62. package/services/utils/forms.json +29 -0
  63. package/services/utils/parametersOptions.json +134 -0
  64. package/services/utils/unknownComponent.json +11 -0
@@ -0,0 +1,1861 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Documentation.js service
5
+ *
6
+ * @description: A set of functions similar to controller's actions to avoid code duplication.
7
+ */
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const _ = require('lodash');
11
+ const moment = require('moment');
12
+ const pathToRegexp = require('path-to-regexp');
13
+ const defaultSettings = require('../config/settings.json');
14
+ const defaultComponents = require('./utils/components.json');
15
+ const form = require('./utils/forms.json');
16
+ const parametersOptions = require('./utils/parametersOptions.json');
17
+
18
+ // keys to pick from the extended config
19
+ const defaultSettingsKeys = Object.keys(defaultSettings);
20
+ const customIsEqual = (obj1, obj2) => _.isEqualWith(obj1, obj2, customComparator);
21
+
22
+ const customComparator = (value1, value2) => {
23
+ if (_.isArray(value1) && _.isArray(value2)) {
24
+ if (value1.length !== value2.length) {
25
+ return false;
26
+ }
27
+ return value1.every(el1 => value2.findIndex(el2 => customIsEqual(el1, el2)) >= 0);
28
+ }
29
+ };
30
+
31
+ module.exports = {
32
+ areObjectsEquals: function(obj1, obj2) {
33
+ // stringify to remove nested empty objects
34
+ return customIsEqual(this.cleanObject(obj1), this.cleanObject(obj2));
35
+ },
36
+
37
+ cleanObject: obj => JSON.parse(JSON.stringify(obj)),
38
+
39
+ arrayCustomizer: (objValue, srcValue) => {
40
+ if (_.isArray(objValue)) return objValue.concat(srcValue);
41
+ },
42
+
43
+ checkIfAPIDocNeedsUpdate: function(apiName) {
44
+ const prevDocumentation = this.createDocObject(this.retrieveDocumentation(apiName));
45
+ const currentDocumentation = this.createDocObject(this.createDocumentationFile(apiName, false));
46
+
47
+ return !this.areObjectsEquals(prevDocumentation, currentDocumentation);
48
+ },
49
+
50
+ /**
51
+ * Check if the documentation folder with its related version of an API exists
52
+ * @param {String} apiName
53
+ */
54
+ checkIfDocumentationFolderExists: function(apiName) {
55
+ try {
56
+ fs.accessSync(this.getDocumentationPath(apiName));
57
+ return true;
58
+ } catch (err) {
59
+ return false;
60
+ }
61
+ },
62
+
63
+ checkIfPluginDocumentationFolderExists: function(pluginName) {
64
+ try {
65
+ fs.accessSync(this.getPluginDocumentationPath(pluginName));
66
+ return true;
67
+ } catch (err) {
68
+ return false;
69
+ }
70
+ },
71
+
72
+ checkIfPluginDocNeedsUpdate: function(pluginName) {
73
+ const prevDocumentation = this.createDocObject(this.retrieveDocumentation(pluginName, true));
74
+ const currentDocumentation = this.createDocObject(
75
+ this.createPluginDocumentationFile(pluginName, false)
76
+ );
77
+
78
+ return !this.areObjectsEquals(prevDocumentation, currentDocumentation);
79
+ },
80
+
81
+ checkIfApiDefaultDocumentationFileExist: function(apiName, docName) {
82
+ try {
83
+ fs.accessSync(this.getAPIOverrideDocumentationPath(apiName, docName));
84
+ return true;
85
+ } catch (err) {
86
+ return false;
87
+ }
88
+ },
89
+
90
+ checkIfPluginDefaultDocumentFileExists: function(pluginName, docName) {
91
+ try {
92
+ fs.accessSync(this.getPluginOverrideDocumentationPath(pluginName, docName));
93
+ return true;
94
+ } catch (err) {
95
+ return false;
96
+ }
97
+ },
98
+
99
+ /**
100
+ * Check if the documentation folder exists in the documentation plugin
101
+ * @returns {Boolean}
102
+ */
103
+ checkIfMergedDocumentationFolderExists: function() {
104
+ try {
105
+ fs.accessSync(this.getMergedDocumentationPath());
106
+ return true;
107
+ } catch (err) {
108
+ return false;
109
+ }
110
+ },
111
+
112
+ /**
113
+ * Recursively create missing directories
114
+ * @param {String} targetDir
115
+ *
116
+ */
117
+ createDocumentationDirectory: function(targetDir) {
118
+ const sep = path.sep;
119
+ const initDir = path.isAbsolute(targetDir) ? sep : '';
120
+ const baseDir = '.';
121
+
122
+ return targetDir.split(sep).reduce((parentDir, childDir) => {
123
+ const curDir = path.resolve(baseDir, parentDir, childDir);
124
+
125
+ try {
126
+ fs.mkdirSync(curDir);
127
+ } catch (err) {
128
+ if (err.code === 'EEXIST') {
129
+ // curDir already exists!
130
+ return curDir;
131
+ }
132
+
133
+ // To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows.
134
+ if (err.code === 'ENOENT') {
135
+ // Throw the original parentDir error on curDir `ENOENT` failure.
136
+ throw new Error(
137
+ `Impossible to create the documentation folder in '${parentDir}', please check the permissions.`
138
+ );
139
+ }
140
+
141
+ const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1;
142
+
143
+ if (!caughtErr || (caughtErr && targetDir === curDir)) {
144
+ throw err; // Throw if it's just the last created dir.
145
+ }
146
+ }
147
+
148
+ return curDir;
149
+ }, initDir);
150
+ },
151
+
152
+ /**
153
+ * Create the apiName.json and unclassified.json files inside an api's documentation/version folder
154
+ * @param {String} apiName
155
+ */
156
+ createDocumentationFile: function(apiName, writeFile = true) {
157
+ // Retrieve all the routes from an API
158
+ const apiRoutes = this.getApiRoutes(apiName);
159
+ const apiDocumentation = this.generateApiDocumentation(apiName, apiRoutes);
160
+
161
+ return Object.keys(apiDocumentation).reduce((acc, docName) => {
162
+ const targetFile = path.resolve(this.getDocumentationPath(apiName), `${docName}.json`);
163
+ // Create the components object in each documentation file when we can create it
164
+ const components =
165
+ strapi.models[docName] !== undefined ? this.generateResponseComponent(docName) : {};
166
+ const tags = docName.split('-').length > 1 ? [] : this.generateTags(apiName, docName);
167
+ const documentation = Object.assign(apiDocumentation[docName], components, { tags });
168
+
169
+ try {
170
+ if (writeFile) {
171
+ return fs.writeFileSync(targetFile, JSON.stringify(documentation, null, 2), 'utf8');
172
+ } else {
173
+ return acc.concat(documentation);
174
+ }
175
+ } catch (err) {
176
+ return acc;
177
+ }
178
+ }, []);
179
+ },
180
+
181
+ createPluginDocumentationFile: function(pluginName, writeFile = true) {
182
+ const pluginRoutes = this.getPluginRoutesWithDescription(pluginName);
183
+ const pluginDocumentation = this.generatePluginDocumentation(pluginName, pluginRoutes);
184
+
185
+ return Object.keys(pluginDocumentation).reduce((acc, docName) => {
186
+ const targetFile = path.resolve(
187
+ this.getPluginDocumentationPath(pluginName),
188
+ `${docName}.json`
189
+ );
190
+ const components =
191
+ _.get(strapi, this.getModelForPlugin(docName, pluginName)) !== undefined &&
192
+ pluginName !== 'upload'
193
+ ? this.generateResponseComponent(docName, pluginName, true)
194
+ : {};
195
+ const [plugin, name] = this.getModelAndNameForPlugin(docName, pluginName);
196
+ const tags =
197
+ docName !== 'unclassified'
198
+ ? this.generateTags(plugin, docName, _.upperFirst(this.formatTag(plugin, name)), true)
199
+ : [];
200
+ const documentation = Object.assign(pluginDocumentation[docName], components, { tags });
201
+
202
+ try {
203
+ if (writeFile) {
204
+ return fs.writeFileSync(targetFile, JSON.stringify(documentation, null, 2), 'utf8');
205
+ } else {
206
+ return acc.concat(documentation);
207
+ }
208
+ } catch (err) {
209
+ // Silent
210
+ }
211
+ }, []);
212
+ },
213
+
214
+ createDocObject: function(array) {
215
+ // use custom merge for arrays
216
+ return array.reduce((acc, curr) => _.mergeWith(acc, curr, this.arrayCustomizer), {});
217
+ },
218
+
219
+ deleteDocumentation: async function(version = this.getDocumentationVersion()) {
220
+ const recursiveDeleteFiles = async (folderPath, removeCompleteFolder = true) => {
221
+ // Check if folderExist
222
+ try {
223
+ const arrayOfPromises = [];
224
+ fs.accessSync(folderPath);
225
+ const items = fs.readdirSync(folderPath).filter(x => x[0] !== '.');
226
+
227
+ items.forEach(item => {
228
+ const itemPath = path.join(folderPath, item);
229
+
230
+ // Check if directory
231
+ if (fs.lstatSync(itemPath).isDirectory()) {
232
+ if (removeCompleteFolder) {
233
+ return arrayOfPromises.push(recursiveDeleteFiles(itemPath), removeCompleteFolder);
234
+ } else if (!itemPath.includes('overrides')) {
235
+ return arrayOfPromises.push(recursiveDeleteFiles(itemPath), removeCompleteFolder);
236
+ }
237
+ } else {
238
+ // Delete all files
239
+ try {
240
+ fs.unlinkSync(itemPath);
241
+ } catch (err) {
242
+ console.log('Cannot delete file', err);
243
+ }
244
+ }
245
+ });
246
+
247
+ await Promise.all(arrayOfPromises);
248
+
249
+ try {
250
+ if (removeCompleteFolder) {
251
+ fs.rmdirSync(folderPath);
252
+ }
253
+ } catch (err) {
254
+ // console.log(err);
255
+ }
256
+ } catch (err) {
257
+ // console.log('The folder does not exist');
258
+ }
259
+ };
260
+
261
+ const arrayOfPromises = [];
262
+
263
+ // Delete api's documentation
264
+ const apis = this.getApis();
265
+ const plugins = this.getPluginsWithDocumentationNeeded();
266
+
267
+ apis.forEach(api => {
268
+ const apiPath = path.join(strapi.config.appPath, 'api', api, 'documentation', version);
269
+ arrayOfPromises.push(recursiveDeleteFiles(apiPath));
270
+ });
271
+
272
+ plugins.forEach(plugin => {
273
+ const pluginPath = path.join(
274
+ strapi.config.appPath,
275
+ 'extensions',
276
+ plugin,
277
+ 'documentation',
278
+ version
279
+ );
280
+
281
+ if (version !== '1.0.0') {
282
+ arrayOfPromises.push(recursiveDeleteFiles(pluginPath));
283
+ } else {
284
+ arrayOfPromises.push(recursiveDeleteFiles(pluginPath, false));
285
+ }
286
+ });
287
+
288
+ const fullDocPath = path.join(
289
+ strapi.config.appPath,
290
+ 'extensions',
291
+ 'documentation',
292
+ 'documentation',
293
+ version
294
+ );
295
+ arrayOfPromises.push(recursiveDeleteFiles(fullDocPath));
296
+
297
+ return await Promise.all(arrayOfPromises);
298
+ },
299
+
300
+ /**
301
+ *
302
+ * Wrap endpoints variables in curly braces
303
+ * @param {String} endPoint
304
+ * @returns {String} (/products/{id})
305
+ */
306
+ formatApiEndPoint: endPoint => {
307
+ return pathToRegexp
308
+ .parse(endPoint)
309
+ .map(token => {
310
+ if (_.isObject(token)) {
311
+ return token.prefix + '{' + token.name + '}'; // eslint-disable-line prefer-template
312
+ }
313
+
314
+ return token;
315
+ })
316
+ .join('');
317
+ },
318
+
319
+ /**
320
+ * Format a plugin model for example users-permissions, user => Users-Permissions - User
321
+ * @param {Sting} plugin
322
+ * @param {String} name
323
+ * @param {Boolean} withoutSpace
324
+ * @return {String}
325
+ */
326
+ formatTag: (plugin, name, withoutSpace = false) => {
327
+ const formattedPluginName = plugin
328
+ .split('-')
329
+ .map(i => _.upperFirst(i))
330
+ .join('');
331
+ const formattedName = _.upperFirst(name);
332
+
333
+ if (withoutSpace) {
334
+ return `${formattedPluginName}${formattedName}`;
335
+ }
336
+
337
+ return `${formattedPluginName} - ${formattedName}`;
338
+ },
339
+
340
+ generateAssociationSchema: function(attributes, getter) {
341
+ return Object.keys(attributes).reduce(
342
+ (acc, curr) => {
343
+ const attribute = attributes[curr];
344
+ const isField = !_.has(attribute, 'model') && !_.has(attribute, 'collection');
345
+
346
+ if (attribute.required) {
347
+ acc.required.push(curr);
348
+ }
349
+
350
+ if (isField) {
351
+ acc.properties[curr] = { type: this.getType(attribute.type), enum: attribute.enum };
352
+ } else {
353
+ const newGetter = getter.slice();
354
+ newGetter.splice(newGetter.length - 1, 1, 'associations');
355
+ const relationNature = _.get(strapi, newGetter).filter(
356
+ association => association.alias === curr
357
+ )[0].nature;
358
+
359
+ switch (relationNature) {
360
+ case 'manyToMany':
361
+ case 'oneToMany':
362
+ case 'manyWay':
363
+ case 'manyToManyMorph':
364
+ acc.properties[curr] = {
365
+ type: 'array',
366
+ items: { type: 'string' },
367
+ };
368
+ break;
369
+ default:
370
+ acc.properties[curr] = { type: 'string' };
371
+ }
372
+ }
373
+
374
+ return acc;
375
+ },
376
+ { required: ['id'], properties: { id: { type: 'string' } } }
377
+ );
378
+ },
379
+
380
+ /**
381
+ * Creates the paths object with all the needed information
382
+ * The object has the following structure { apiName: { paths: {} }, knownTag1: { paths: {} }, unclassified: { paths: {} } }
383
+ * Each key will create a documentation.json file
384
+ *
385
+ * @param {String} apiName
386
+ * @param {Array} routes
387
+ * @returns {Object}
388
+ */
389
+ generateApiDocumentation: function(apiName, routes) {
390
+ return routes.reduce((acc, current) => {
391
+ const [controllerName, controllerMethod] = current.handler.split('.');
392
+ // Retrieve the tag key in the config object
393
+ const routeTagConfig = _.get(current, ['config', 'tag']);
394
+ // Add curly braces between dynamic params
395
+ const endPoint = this.formatApiEndPoint(current.path);
396
+ let verb;
397
+
398
+ if (Array.isArray(current.method)) {
399
+ verb = current.method.map(method => method.toLowerCase());
400
+ } else {
401
+ verb = current.method.toLowerCase();
402
+ }
403
+ // The key corresponds to firsts keys of the returned object
404
+ let key;
405
+ let tags;
406
+
407
+ if (controllerName.toLowerCase() === apiName && !_.isObject(routeTagConfig)) {
408
+ key = apiName;
409
+ } else if (routeTagConfig !== undefined) {
410
+ if (_.isObject(routeTagConfig)) {
411
+ const { name, plugin } = routeTagConfig;
412
+ const referencePlugin = !_.isEmpty(plugin);
413
+
414
+ key = referencePlugin ? `${plugin}-${name}` : name.toLowerCase();
415
+ tags = referencePlugin ? this.formatTag(plugin, name) : _.upperFirst(name);
416
+ } else {
417
+ key = routeTagConfig.toLowerCase();
418
+ }
419
+ } else {
420
+ key = 'unclassified';
421
+ }
422
+
423
+ const verbObject = {
424
+ deprecated: false,
425
+ description: this.generateVerbDescription(
426
+ verb,
427
+ current.handler,
428
+ key,
429
+ endPoint.split('/')[1],
430
+ _.get(current, 'config.description')
431
+ ),
432
+ responses: this.generateResponses(verb, current, key),
433
+ summary: '',
434
+ tags: _.isEmpty(tags) ? [_.upperFirst(key)] : [_.upperFirst(tags)],
435
+ };
436
+
437
+ // Swagger is not support key with ',' symbol, for array of methods need generate documentation for each method
438
+ if (Array.isArray(verb)) {
439
+ verb.forEach(method => {
440
+ _.set(acc, [key, 'paths', endPoint, method], verbObject);
441
+ });
442
+ } else {
443
+ _.set(acc, [key, 'paths', endPoint, verb], verbObject);
444
+ }
445
+
446
+ if (verb.includes('post') || verb.includes('put')) {
447
+ let requestBody;
448
+
449
+ if (controllerMethod === 'create' || controllerMethod === 'update') {
450
+ requestBody = {
451
+ description: '',
452
+ required: true,
453
+ content: {
454
+ 'application/json': {
455
+ schema: {
456
+ $ref: `#/components/schemas/New${_.upperFirst(key)}`,
457
+ },
458
+ },
459
+ },
460
+ };
461
+ } else {
462
+ requestBody = {
463
+ description: '',
464
+ required: true,
465
+ content: {
466
+ 'application/json': {
467
+ schema: {
468
+ properties: {
469
+ foo: {
470
+ type: 'string',
471
+ },
472
+ },
473
+ },
474
+ },
475
+ },
476
+ };
477
+ }
478
+
479
+ if (Array.isArray(verb)) {
480
+ verb.forEach(method => {
481
+ _.set(acc, [key, 'paths', endPoint, method, 'requestBody'], requestBody);
482
+ });
483
+ } else {
484
+ _.set(acc, [key, 'paths', endPoint, verb, 'requestBody'], requestBody);
485
+ }
486
+ }
487
+
488
+ // Refer to https://swagger.io/specification/#pathItemObject
489
+ const parameters = this.generateVerbParameters(verb, controllerMethod, current.path);
490
+
491
+ if (!verb.includes('post')) {
492
+ if (Array.isArray(verb)) {
493
+ verb.forEach(method => {
494
+ _.set(acc, [key, 'paths', endPoint, method, 'parameters'], parameters);
495
+ });
496
+ } else {
497
+ _.set(acc, [key, 'paths', endPoint, verb, 'parameters'], parameters);
498
+ }
499
+ }
500
+
501
+ return acc;
502
+ }, {});
503
+ },
504
+
505
+ generateFullDoc: function(version = this.getDocumentationVersion()) {
506
+ const apisDoc = this.retrieveDocumentationFiles(false, version);
507
+ const pluginsDoc = this.retrieveDocumentationFiles(true, version);
508
+ const appDoc = [...apisDoc, ...pluginsDoc];
509
+ const defaultSettings = _.cloneDeep(
510
+ _.pick(strapi.plugins.documentation.config, defaultSettingsKeys)
511
+ );
512
+ _.set(defaultSettings, ['info', 'x-generation-date'], moment().format('L LTS'));
513
+ _.set(defaultSettings, ['info', 'version'], version);
514
+ const tags = appDoc.reduce((acc, current) => {
515
+ const tags = current.tags.filter(el => {
516
+ return _.findIndex(acc, ['name', el.name || '']) === -1;
517
+ });
518
+
519
+ return acc.concat(tags);
520
+ }, []);
521
+ const fullDoc = _.merge(
522
+ appDoc.reduce((acc, current) => {
523
+ return _.merge(acc, current);
524
+ }, defaultSettings),
525
+ defaultComponents
526
+ // { tags },
527
+ );
528
+
529
+ fullDoc.tags = tags;
530
+
531
+ return fullDoc;
532
+ },
533
+ /**
534
+ * Generate the main component that has refs to sub components
535
+ * @param {Object} attributes
536
+ * @param {Array} associations
537
+ * @returns {Object}
538
+ */
539
+ generateMainComponent: function(attributes, associations) {
540
+ return Object.keys(attributes).reduce(
541
+ (acc, current) => {
542
+ const attribute = attributes[current];
543
+ // Refer to https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#dataTypes
544
+ const type = this.getType(attribute.type);
545
+ const {
546
+ description,
547
+ default: defaultValue,
548
+ minimum,
549
+ maxmimun,
550
+ maxLength,
551
+ minLength,
552
+ enum: enumeration,
553
+ } = attribute;
554
+
555
+ if (attribute.required === true) {
556
+ acc.required.push(current);
557
+ }
558
+
559
+ if (attribute.model || attribute.collection) {
560
+ const currentAssociation = associations.filter(
561
+ association => association.alias === current
562
+ )[0];
563
+ const relationNature = currentAssociation.nature;
564
+ const name = currentAssociation.model || currentAssociation.collection;
565
+ const getter =
566
+ currentAssociation.plugin !== undefined
567
+ ? currentAssociation.plugin === 'admin'
568
+ ? ['admin', 'models', name, 'attributes']
569
+ : ['plugins', currentAssociation.plugin, 'models', name, 'attributes']
570
+ : ['models', name.toLowerCase(), 'attributes'];
571
+ const associationAttributes = _.get(strapi, getter);
572
+ const associationSchema = this.generateAssociationSchema(associationAttributes, getter);
573
+
574
+ switch (relationNature) {
575
+ case 'manyToMany':
576
+ case 'oneToMany':
577
+ case 'manyWay':
578
+ case 'manyToManyMorph':
579
+ acc.properties[current] = {
580
+ type: 'array',
581
+ items: associationSchema,
582
+ };
583
+ break;
584
+ default:
585
+ acc.properties[current] = associationSchema;
586
+ }
587
+ } else if (type === 'component') {
588
+ const { repeatable, component, min, max } = attribute;
589
+
590
+ const cmp = this.generateMainComponent(
591
+ strapi.components[component].attributes,
592
+ strapi.components[component].associations
593
+ );
594
+
595
+ if (repeatable) {
596
+ acc.properties[current] = {
597
+ type: 'array',
598
+ items: {
599
+ type: 'object',
600
+ ...cmp,
601
+ },
602
+ minItems: min,
603
+ maxItems: max,
604
+ };
605
+ } else {
606
+ acc.properties[current] = {
607
+ type: 'object',
608
+ ...cmp,
609
+ description,
610
+ };
611
+ }
612
+ } else if (type === 'dynamiczone') {
613
+ const { components, min, max } = attribute;
614
+
615
+ const cmps = components.map(component => {
616
+ const schema = this.generateMainComponent(
617
+ strapi.components[component].attributes,
618
+ strapi.components[component].associations
619
+ );
620
+
621
+ return _.merge(
622
+ {
623
+ properties: {
624
+ __component: {
625
+ type: 'string',
626
+ enum: components,
627
+ },
628
+ },
629
+ },
630
+ schema
631
+ );
632
+ });
633
+
634
+ acc.properties[current] = {
635
+ type: 'array',
636
+ items: {
637
+ oneOf: cmps,
638
+ },
639
+ minItems: min,
640
+ maxItems: max,
641
+ };
642
+ } else {
643
+ acc.properties[current] = {
644
+ type,
645
+ format: this.getFormat(attribute.type),
646
+ description,
647
+ default: defaultValue,
648
+ minimum,
649
+ maxmimun,
650
+ maxLength,
651
+ minLength,
652
+ enum: enumeration,
653
+ };
654
+ }
655
+
656
+ return acc;
657
+ },
658
+ { required: ['id'], properties: { id: { type: 'string' } } }
659
+ );
660
+ },
661
+
662
+ generatePluginDocumentation: function(pluginName, routes) {
663
+ return routes.reduce((acc, current) => {
664
+ const {
665
+ config: { description, prefix },
666
+ } = current;
667
+ const endPoint =
668
+ prefix === undefined
669
+ ? this.formatApiEndPoint(`/${pluginName}${current.path}`)
670
+ : this.formatApiEndPoint(`${prefix}${current.path}`);
671
+ let verb;
672
+
673
+ if (Array.isArray(current.method)) {
674
+ verb = current.method.map(method => method.toLowerCase());
675
+ } else {
676
+ verb = current.method.toLowerCase();
677
+ }
678
+
679
+ const actionType = _.get(current, ['config', 'tag', 'actionType'], '');
680
+ let key;
681
+ let tags;
682
+
683
+ if (_.isObject(current.config.tag)) {
684
+ const { name, plugin } = current.config.tag;
685
+ key = plugin ? `${plugin}-${name}` : name;
686
+ tags = plugin ? [this.formatTag(plugin, name)] : [name];
687
+ } else {
688
+ const tag = current.config.tag;
689
+ key = !_.isEmpty(tag) ? tag : 'unclassified';
690
+ tags = !_.isEmpty(tag) ? [tag] : ['Unclassified'];
691
+ }
692
+
693
+ const hasDefaultDocumentation = this.checkIfPluginDefaultDocumentFileExists(pluginName, key);
694
+ const defaultDocumentation = hasDefaultDocumentation
695
+ ? this.getPluginDefaultVerbDocumentation(pluginName, key, endPoint, verb)
696
+ : null;
697
+ const verbObject = {
698
+ deprecated: false,
699
+ description,
700
+ responses: this.generatePluginVerbResponses(current),
701
+ summary: '',
702
+ tags,
703
+ };
704
+
705
+ _.set(acc, [key, 'paths', endPoint, verb], verbObject);
706
+
707
+ const parameters = this.generateVerbParameters(
708
+ verb,
709
+ actionType,
710
+ `/${pluginName}${current.path}`
711
+ );
712
+
713
+ if (_.isEmpty(defaultDocumentation)) {
714
+ if (!verb.includes('post')) {
715
+ if (Array.isArray(verb)) {
716
+ verb.forEach(method => {
717
+ _.set(acc, [key, 'paths', endPoint, method, 'parameters'], parameters);
718
+ });
719
+ } else {
720
+ _.set(acc, [key, 'paths', endPoint, verb, 'parameters'], parameters);
721
+ }
722
+ }
723
+
724
+ if (verb.includes('post') || verb.includes('put')) {
725
+ let requestBody;
726
+
727
+ if (actionType === 'create' || actionType === 'update') {
728
+ const { name, plugin } = _.isObject(current.config.tag)
729
+ ? current.config.tag
730
+ : { tag: current.config.tag };
731
+ const $ref = plugin
732
+ ? `#/components/schemas/New${this.formatTag(plugin, name, true)}`
733
+ : `#/components/schemas/New${_.upperFirst(name)}`;
734
+ requestBody = {
735
+ description: '',
736
+ required: true,
737
+ content: {
738
+ 'application/json': {
739
+ schema: {
740
+ $ref,
741
+ },
742
+ },
743
+ },
744
+ };
745
+ } else {
746
+ requestBody = {
747
+ description: '',
748
+ required: true,
749
+ content: {
750
+ 'application/json': {
751
+ schema: {
752
+ properties: {
753
+ foo: {
754
+ type: 'string',
755
+ },
756
+ },
757
+ },
758
+ },
759
+ },
760
+ };
761
+ }
762
+
763
+ if (Array.isArray(verb)) {
764
+ verb.forEach(method => {
765
+ _.set(acc, [key, 'paths', endPoint, method, 'requestBody'], requestBody);
766
+ });
767
+ } else {
768
+ _.set(acc, [key, 'paths', endPoint, verb, 'requestBody'], requestBody);
769
+ }
770
+ }
771
+ }
772
+
773
+ return acc;
774
+ }, {});
775
+ },
776
+
777
+ generatePluginResponseSchema: function(tag) {
778
+ const { actionType, name, plugin } = _.isObject(tag) ? tag : { tag };
779
+ const getter = plugin ? ['plugins', plugin, 'models', name.toLowerCase()] : ['models', name];
780
+ const isModelRelated =
781
+ _.get(strapi, getter) !== undefined &&
782
+ ['find', 'findOne', 'create', 'search', 'update', 'destroy', 'count'].includes(actionType);
783
+ const $ref = plugin
784
+ ? `#/components/schemas/${this.formatTag(plugin, name, true)}`
785
+ : `#/components/schemas/${_.upperFirst(name)}`;
786
+
787
+ if (isModelRelated) {
788
+ switch (actionType) {
789
+ case 'find':
790
+ return {
791
+ type: 'array',
792
+ items: {
793
+ $ref,
794
+ },
795
+ };
796
+ case 'count':
797
+ return {
798
+ properties: {
799
+ count: {
800
+ type: 'integer',
801
+ },
802
+ },
803
+ };
804
+ case 'findOne':
805
+ case 'update':
806
+ case 'create':
807
+ return {
808
+ $ref,
809
+ };
810
+ default:
811
+ return {
812
+ properties: {
813
+ foo: {
814
+ type: 'string',
815
+ },
816
+ },
817
+ };
818
+ }
819
+ }
820
+
821
+ return {
822
+ properties: {
823
+ foo: {
824
+ type: 'string',
825
+ },
826
+ },
827
+ };
828
+ },
829
+
830
+ generatePluginVerbResponses: function(routeObject) {
831
+ const {
832
+ config: { tag },
833
+ } = routeObject;
834
+ const actionType = _.get(tag, 'actionType');
835
+ let schema;
836
+
837
+ if (!tag || !actionType) {
838
+ schema = {
839
+ properties: {
840
+ foo: {
841
+ type: 'string',
842
+ },
843
+ },
844
+ };
845
+ } else {
846
+ schema = this.generatePluginResponseSchema(tag);
847
+ }
848
+
849
+ const response = {
850
+ 200: {
851
+ description: 'response',
852
+ content: {
853
+ 'application/json': {
854
+ schema,
855
+ },
856
+ },
857
+ },
858
+ 403: {
859
+ description: 'Forbidden',
860
+ content: {
861
+ 'application/json': {
862
+ schema: {
863
+ $ref: '#/components/schemas/Error',
864
+ },
865
+ },
866
+ },
867
+ },
868
+ 404: {
869
+ description: 'Not found',
870
+ content: {
871
+ 'application/json': {
872
+ schema: {
873
+ $ref: '#/components/schemas/Error',
874
+ },
875
+ },
876
+ },
877
+ },
878
+ };
879
+
880
+ const { generateDefaultResponse } = strapi.plugins.documentation.config['x-strapi-config'];
881
+
882
+ if (generateDefaultResponse) {
883
+ response.default = {
884
+ description: 'unexpected error',
885
+ content: {
886
+ 'application/json': {
887
+ schema: {
888
+ $ref: '#/components/schemas/Error',
889
+ },
890
+ },
891
+ },
892
+ };
893
+ }
894
+
895
+ return response;
896
+ },
897
+
898
+ /**
899
+ * Create the response object https://swagger.io/specification/#responsesObject
900
+ * @param {String} verb
901
+ * @param {Object} routeObject
902
+ * @param {String} tag
903
+ * @returns {Object}
904
+ */
905
+ generateResponses: function(verb, routeObject, tag) {
906
+ const endPoint = routeObject.path.split('/')[1];
907
+ const description = this.generateResponseDescription(verb, tag, endPoint);
908
+ const schema = this.generateResponseSchema(verb, routeObject, tag, endPoint);
909
+
910
+ const response = {
911
+ 200: {
912
+ description,
913
+ content: {
914
+ 'application/json': {
915
+ schema,
916
+ },
917
+ },
918
+ },
919
+ 403: {
920
+ description: 'Forbidden',
921
+ content: {
922
+ 'application/json': {
923
+ schema: {
924
+ $ref: '#/components/schemas/Error',
925
+ },
926
+ },
927
+ },
928
+ },
929
+ 404: {
930
+ description: 'Not found',
931
+ content: {
932
+ 'application/json': {
933
+ schema: {
934
+ $ref: '#/components/schemas/Error',
935
+ },
936
+ },
937
+ },
938
+ },
939
+ };
940
+
941
+ const { generateDefaultResponse } = strapi.plugins.documentation.config['x-strapi-config'];
942
+
943
+ if (generateDefaultResponse) {
944
+ response.default = {
945
+ description: 'unexpected error',
946
+ content: {
947
+ 'application/json': {
948
+ schema: {
949
+ $ref: '#/components/schemas/Error',
950
+ },
951
+ },
952
+ },
953
+ };
954
+ }
955
+
956
+ return response;
957
+ },
958
+
959
+ /**
960
+ * Retrieve all privates attributes from a model
961
+ * @param {Object} attributes
962
+ */
963
+ getPrivateAttributes: function(attributes) {
964
+ const privateAttributes = Object.keys(attributes).reduce((acc, current) => {
965
+ if (attributes[current].private === true) {
966
+ acc.push(current);
967
+ }
968
+ return acc;
969
+ }, []);
970
+
971
+ return privateAttributes;
972
+ },
973
+
974
+ /**
975
+ * Create a component object with the model's attributes and relations
976
+ * Refer to https://swagger.io/docs/specification/components/
977
+ * @param {String} tag
978
+ * @returns {Object}
979
+ */
980
+ generateResponseComponent: function(tag, pluginName = '', isPlugin = false) {
981
+ // The component's name have to be capitalised
982
+ const [plugin, name] = isPlugin ? this.getModelAndNameForPlugin(tag, pluginName) : [null, null];
983
+ const upperFirstTag = isPlugin ? this.formatTag(plugin, name, true) : _.upperFirst(tag);
984
+ const attributesGetter = isPlugin
985
+ ? [...this.getModelForPlugin(tag, plugin), 'attributes']
986
+ : ['models', tag, 'attributes'];
987
+ const associationGetter = isPlugin
988
+ ? [...this.getModelForPlugin(tag, plugin), 'associations']
989
+ : ['models', tag, 'associations'];
990
+ const attributesObject = _.get(strapi, attributesGetter);
991
+ const privateAttributes = this.getPrivateAttributes(attributesObject);
992
+ const modelAssociations = _.get(strapi, associationGetter);
993
+ const { attributes } = this.getModelAttributes(attributesObject);
994
+ const associationsWithUpload = modelAssociations
995
+ .filter(association => {
996
+ return association.plugin === 'upload';
997
+ })
998
+ .map(obj => obj.alias);
999
+
1000
+ // We always create two nested components from the main one
1001
+ const mainComponent = this.generateMainComponent(attributes, modelAssociations, upperFirstTag);
1002
+
1003
+ // Get Component that doesn't display the privates attributes since a mask is applied
1004
+ // Please refer https://github.com/strapi/strapi/blob/585800b7b98093f596759b296a43f89c491d4f4f/packages/strapi/lib/middlewares/mask/index.js#L92-L100
1005
+ const getComponent = Object.keys(mainComponent.properties).reduce(
1006
+ (acc, current) => {
1007
+ if (privateAttributes.indexOf(current) === -1) {
1008
+ acc.properties[current] = mainComponent.properties[current];
1009
+ }
1010
+ return acc;
1011
+ },
1012
+ { required: mainComponent.required, properties: {} }
1013
+ );
1014
+
1015
+ // Special component only for POST || PUT verbs since the upload is made with a different route
1016
+ const postComponent = Object.keys(mainComponent).reduce((acc, current) => {
1017
+ if (current === 'required') {
1018
+ const required = mainComponent.required.slice().filter(attr => {
1019
+ return associationsWithUpload.indexOf(attr) === -1 && attr !== 'id' && attr !== '_id';
1020
+ });
1021
+
1022
+ if (required.length > 0) {
1023
+ acc.required = required;
1024
+ }
1025
+ }
1026
+
1027
+ if (current === 'properties') {
1028
+ const properties = Object.keys(mainComponent.properties).reduce((acc, current) => {
1029
+ if (
1030
+ associationsWithUpload.indexOf(current) === -1 &&
1031
+ current !== 'id' &&
1032
+ current !== '_id'
1033
+ ) {
1034
+ // The post request shouldn't include nested relations of type 2
1035
+ // For instance if a product has many tags
1036
+ // we expect to find an array of tags objects containing other relations in the get response
1037
+ // and since we use to getComponent to generate this one we need to
1038
+ // remove this object since we only send an array of tag ids.
1039
+ if (_.find(modelAssociations, ['alias', current])) {
1040
+ const isArrayProperty =
1041
+ _.get(mainComponent, ['properties', current, 'type']) !== undefined;
1042
+
1043
+ if (isArrayProperty) {
1044
+ acc[current] = { type: 'array', items: { type: 'string' } };
1045
+ } else {
1046
+ acc[current] = { type: 'string' };
1047
+ }
1048
+ } else {
1049
+ // If the field is not an association we take the one from the component
1050
+ acc[current] = mainComponent.properties[current];
1051
+ }
1052
+ }
1053
+
1054
+ return acc;
1055
+ }, {});
1056
+
1057
+ acc.properties = properties;
1058
+ }
1059
+
1060
+ return acc;
1061
+ }, {});
1062
+
1063
+ return {
1064
+ components: {
1065
+ schemas: {
1066
+ [upperFirstTag]: getComponent,
1067
+ [`New${upperFirstTag}`]: postComponent,
1068
+ },
1069
+ },
1070
+ };
1071
+ },
1072
+
1073
+ /**
1074
+ * Generate a better description for a response when we can guess what's the user is going to retrieve
1075
+ * @param {String} verb
1076
+ * @param {String} tag
1077
+ * @param {String} endPoint
1078
+ * @returns {String}
1079
+ */
1080
+ generateResponseDescription: function(verb, tag, endPoint) {
1081
+ const isModelRelated = strapi.models[tag] !== undefined && tag === endPoint;
1082
+
1083
+ if (Array.isArray(verb)) {
1084
+ verb = verb.map(method => method.toLocaleLowerCase());
1085
+ }
1086
+
1087
+ if (verb.includes('get') || verb.includes('put') || verb.includes('post')) {
1088
+ return isModelRelated ? `Retrieve ${tag} document(s)` : 'response';
1089
+ } else if (verb.includes('delete')) {
1090
+ return isModelRelated
1091
+ ? `deletes a single ${tag} based on the ID supplied`
1092
+ : 'deletes a single record based on the ID supplied';
1093
+ } else {
1094
+ return 'response';
1095
+ }
1096
+ },
1097
+
1098
+ /**
1099
+ * For each response generate its schema
1100
+ * Its schema is either a component when we know what the routes returns otherwise, it returns a dummy schema
1101
+ * that the user will modify later
1102
+ * @param {String} verb
1103
+ * @param {Object} route
1104
+ * @param {String} tag
1105
+ * @param {String} endPoint
1106
+ * @returns {Object}
1107
+ */
1108
+ generateResponseSchema: function(verb, routeObject, tag) {
1109
+ const { handler } = routeObject;
1110
+ let [controller, handlerMethod] = handler.split('.');
1111
+ let upperFirstTag = _.upperFirst(tag);
1112
+
1113
+ if (verb === 'delete') {
1114
+ return {
1115
+ type: 'integer',
1116
+ format: 'int64',
1117
+ };
1118
+ }
1119
+
1120
+ // A tag key might be added to a route to tell if a custom endPoint in an api/<model>/config/routes.json
1121
+ // Retrieves data from another model it is a faster way to generate the response
1122
+ const routeReferenceTag = _.get(routeObject, ['config', 'tag']);
1123
+ let isModelRelated = false;
1124
+ const shouldCheckIfACustomEndPointReferencesAnotherModel =
1125
+ _.isObject(routeReferenceTag) && !_.isEmpty(_.get(routeReferenceTag, 'name'));
1126
+
1127
+ if (shouldCheckIfACustomEndPointReferencesAnotherModel) {
1128
+ const { actionType, name, plugin } = routeReferenceTag;
1129
+ // A model could be in either a plugin or the api folder
1130
+ // The path is different depending on the case
1131
+ const getter = !_.isEmpty(plugin)
1132
+ ? ['plugins', plugin, 'models', name.toLowerCase()]
1133
+ : ['models', name.toLowerCase()];
1134
+
1135
+ // An actionType key might be added to the tag object to guide the algorithm is generating an automatic response
1136
+ const isKnownAction = [
1137
+ 'find',
1138
+ 'findOne',
1139
+ 'create',
1140
+ 'search',
1141
+ 'update',
1142
+ 'destroy',
1143
+ 'count',
1144
+ ].includes(actionType);
1145
+
1146
+ // Check if a route points to a model
1147
+ isModelRelated = _.get(strapi, getter) !== undefined && isKnownAction;
1148
+
1149
+ if (isModelRelated && isKnownAction) {
1150
+ // We need to change the handlerMethod name if it is know to generate the good schema
1151
+ handlerMethod = actionType;
1152
+
1153
+ // This is to retrieve the correct component if a custom endpoints references a plugin model
1154
+ if (!_.isEmpty(plugin)) {
1155
+ upperFirstTag = this.formatTag(plugin, name, true);
1156
+ }
1157
+ }
1158
+ } else {
1159
+ // Normal way there's no tag object
1160
+ isModelRelated = strapi.models[tag] !== undefined && tag === _.lowerCase(controller);
1161
+ }
1162
+
1163
+ // We create a component when we are sure that we can 'guess' what's needed to be sent
1164
+ // https://swagger.io/specification/#referenceObject
1165
+ if (isModelRelated) {
1166
+ switch (handlerMethod) {
1167
+ case 'find':
1168
+ return {
1169
+ type: 'array',
1170
+ items: {
1171
+ $ref: `#/components/schemas/${upperFirstTag}`,
1172
+ },
1173
+ };
1174
+ case 'count':
1175
+ return {
1176
+ properties: {
1177
+ count: {
1178
+ type: 'integer',
1179
+ },
1180
+ },
1181
+ };
1182
+ case 'findOne':
1183
+ case 'update':
1184
+ case 'create':
1185
+ return {
1186
+ $ref: `#/components/schemas/${upperFirstTag}`,
1187
+ };
1188
+ default:
1189
+ return {
1190
+ properties: {
1191
+ foo: {
1192
+ type: 'string',
1193
+ },
1194
+ },
1195
+ };
1196
+ }
1197
+ }
1198
+
1199
+ return {
1200
+ properties: {
1201
+ foo: {
1202
+ type: 'string',
1203
+ },
1204
+ },
1205
+ };
1206
+ },
1207
+
1208
+ generateTags: function(name, docName, tag = '', isPlugin = false) {
1209
+ return [
1210
+ {
1211
+ name: isPlugin ? tag : _.upperFirst(docName),
1212
+ },
1213
+ ];
1214
+ },
1215
+
1216
+ /**
1217
+ * Add a default description when it's implied
1218
+ *
1219
+ * @param {String} verb
1220
+ * @param {String} handler
1221
+ * @param {String} tag
1222
+ * @param {String} endPoint
1223
+ * @returns {String}
1224
+ */
1225
+ generateVerbDescription: (verb, handler, tag, endPoint, description) => {
1226
+ const isModelRelated = strapi.models[tag] !== undefined && tag === endPoint;
1227
+
1228
+ if (description) {
1229
+ return description;
1230
+ }
1231
+
1232
+ if (Array.isArray(verb)) {
1233
+ const [, controllerMethod] = handler.split('.');
1234
+
1235
+ if ((verb.includes('get') && verb.includes('post')) || controllerMethod === 'findOrCreate') {
1236
+ return `Find or create ${tag} record`;
1237
+ }
1238
+
1239
+ if (
1240
+ (verb.includes('put') && verb.includes('post')) ||
1241
+ controllerMethod === 'createOrUpdate'
1242
+ ) {
1243
+ return `Create or update ${tag} record`;
1244
+ }
1245
+
1246
+ return '';
1247
+ }
1248
+
1249
+ switch (verb) {
1250
+ case 'get': {
1251
+ const [, controllerMethod] = handler.split('.');
1252
+
1253
+ if (isModelRelated) {
1254
+ switch (controllerMethod) {
1255
+ case 'count':
1256
+ return `Retrieve the number of ${tag} documents`;
1257
+ case 'findOne':
1258
+ return `Find one ${tag} record`;
1259
+ case 'find':
1260
+ return `Find all the ${tag}'s records`;
1261
+ default:
1262
+ return '';
1263
+ }
1264
+ }
1265
+
1266
+ return '';
1267
+ }
1268
+ case 'delete':
1269
+ return isModelRelated ? `Delete a single ${tag} record` : 'Delete a record';
1270
+ case 'post':
1271
+ return isModelRelated ? `Create a new ${tag} record` : 'Create a new record';
1272
+ case 'put':
1273
+ return isModelRelated ? `Update a single ${tag} record` : 'Update a record';
1274
+ case 'patch':
1275
+ return '';
1276
+ case 'head':
1277
+ return '';
1278
+ default:
1279
+ return '';
1280
+ }
1281
+ },
1282
+
1283
+ /**
1284
+ * Generate the verb parameters object
1285
+ * Refer to https://swagger.io/specification/#pathItemObject
1286
+ * @param {Sting} verb
1287
+ * @param {String} controllerMethod
1288
+ * @param {String} endPoint
1289
+ */
1290
+ generateVerbParameters: function(verb, controllerMethod, endPoint) {
1291
+ const params = pathToRegexp
1292
+ .parse(endPoint)
1293
+ .filter(token => _.isObject(token))
1294
+ .reduce((acc, current) => {
1295
+ const param = {
1296
+ name: current.name,
1297
+ in: 'path',
1298
+ description: '',
1299
+ deprecated: false,
1300
+ required: true,
1301
+ schema: { type: 'string' },
1302
+ };
1303
+
1304
+ return acc.concat(param);
1305
+ }, []);
1306
+
1307
+ if (verb === 'get' && controllerMethod === 'find') {
1308
+ // parametersOptions corresponds to this section
1309
+ // of the documentation https://strapi.akemona.com/documentation/developer-docs/latest/developer-resources/content-api/content-api.html#filters
1310
+ return [...params, ...parametersOptions];
1311
+ }
1312
+
1313
+ return params;
1314
+ },
1315
+
1316
+ /**
1317
+ * Retrieve the apis in /api
1318
+ * @returns {Array}
1319
+ */
1320
+ getApis: () => {
1321
+ return Object.keys(strapi.api || {});
1322
+ },
1323
+
1324
+ getAPIOverrideComponentsDocumentation: function(apiName, docName) {
1325
+ try {
1326
+ const documentation = JSON.parse(
1327
+ fs.readFileSync(this.getAPIOverrideDocumentationPath(apiName, docName), 'utf8')
1328
+ );
1329
+
1330
+ return _.get(documentation, 'components', null);
1331
+ } catch (err) {
1332
+ return null;
1333
+ }
1334
+ },
1335
+
1336
+ getAPIDefaultTagsDocumentation: function(name, docName) {
1337
+ try {
1338
+ const documentation = JSON.parse(
1339
+ fs.readFileSync(this.getAPIOverrideDocumentationPath(name, docName), 'utf8')
1340
+ );
1341
+
1342
+ return _.get(documentation, 'tags', null);
1343
+ } catch (err) {
1344
+ return null;
1345
+ }
1346
+ },
1347
+
1348
+ getAPIDefaultVerbDocumentation: function(apiName, docName, routePath, verb) {
1349
+ try {
1350
+ const documentation = JSON.parse(
1351
+ fs.readFileSync(this.getAPIOverrideDocumentationPath(apiName, docName), 'utf8')
1352
+ );
1353
+
1354
+ return _.get(documentation, ['paths', routePath, verb], null);
1355
+ } catch (err) {
1356
+ return null;
1357
+ }
1358
+ },
1359
+
1360
+ getAPIOverrideDocumentationPath: function(apiName, docName) {
1361
+ return path.join(
1362
+ strapi.config.appPath,
1363
+ 'api',
1364
+ apiName,
1365
+ 'documentation',
1366
+ 'overrides',
1367
+ this.getDocumentationVersion(),
1368
+ `${docName}.json`
1369
+ );
1370
+ },
1371
+
1372
+ /**
1373
+ * Given an api retrieve its endpoints
1374
+ * @param {String}
1375
+ * @returns {Array}
1376
+ */
1377
+ getApiRoutes: apiName => {
1378
+ return _.get(strapi, ['api', apiName, 'config', 'routes'], []);
1379
+ },
1380
+
1381
+ getDocumentationOverridesPath: function(apiName) {
1382
+ return path.join(
1383
+ strapi.config.appPath,
1384
+ 'api',
1385
+ apiName,
1386
+ 'documentation',
1387
+ this.getDocumentationVersion(),
1388
+ 'overrides'
1389
+ );
1390
+ },
1391
+
1392
+ /**
1393
+ * Given an api from /api retrieve its version directory
1394
+ * @param {String} apiName
1395
+ * @returns {Path}
1396
+ */
1397
+ getDocumentationPath: function(apiName) {
1398
+ return path.join(
1399
+ strapi.config.appPath,
1400
+ 'api',
1401
+ apiName,
1402
+ 'documentation',
1403
+ this.getDocumentationVersion()
1404
+ );
1405
+ },
1406
+
1407
+ getFullDocumentationPath: () => {
1408
+ return path.join(strapi.config.appPath, 'extensions', 'documentation', 'documentation');
1409
+ },
1410
+
1411
+ /**
1412
+ * Retrieve the plugin's configuration version
1413
+ */
1414
+ getDocumentationVersion: () => {
1415
+ const version = strapi.plugins['documentation'].config.info.version;
1416
+
1417
+ return version;
1418
+ },
1419
+
1420
+ /**
1421
+ * Retrieve the documentation plugin documentation directory
1422
+ */
1423
+ getMergedDocumentationPath: function(version = this.getDocumentationVersion()) {
1424
+ return path.join(
1425
+ strapi.config.appPath,
1426
+ 'extensions',
1427
+ 'documentation',
1428
+ 'documentation',
1429
+ version
1430
+ );
1431
+ },
1432
+
1433
+ /**
1434
+ * Retrieve the model's attributes
1435
+ * @param {Objet} modelAttributes
1436
+ * @returns {Object} { associations: [{ name: 'foo', getter: [], tag: 'foos' }], attributes }
1437
+ */
1438
+ getModelAttributes: function(modelAttributes) {
1439
+ const associations = [];
1440
+ const attributes = Object.keys(modelAttributes)
1441
+ .map(attr => {
1442
+ const attribute = modelAttributes[attr];
1443
+ const isField = !_.has(attribute, 'model') && !_.has(attribute, 'collection');
1444
+
1445
+ if (!isField) {
1446
+ const name = attribute.model || attribute.collection;
1447
+ const getter =
1448
+ attribute.plugin !== undefined
1449
+ ? ['plugins', attribute.plugin, 'models', name, 'attributes']
1450
+ : ['models', name, 'attributes'];
1451
+ associations.push({ name, getter, tag: attr });
1452
+ }
1453
+
1454
+ return attr;
1455
+ })
1456
+ .reduce((acc, current) => {
1457
+ acc[current] = modelAttributes[current];
1458
+
1459
+ return acc;
1460
+ }, {});
1461
+
1462
+ return { associations, attributes };
1463
+ },
1464
+
1465
+ /**
1466
+ * Refer to https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#dataTypes
1467
+ * @param {String} type
1468
+ * @returns {String}
1469
+ */
1470
+ getType: type => {
1471
+ switch (type) {
1472
+ case 'string':
1473
+ case 'byte':
1474
+ case 'binary':
1475
+ case 'password':
1476
+ case 'email':
1477
+ case 'text':
1478
+ case 'enumeration':
1479
+ case 'date':
1480
+ case 'datetime':
1481
+ case 'time':
1482
+ case 'richtext':
1483
+ return 'string';
1484
+ case 'float':
1485
+ case 'decimal':
1486
+ case 'double':
1487
+ return 'number';
1488
+ case 'integer':
1489
+ case 'biginteger':
1490
+ case 'long':
1491
+ return 'integer';
1492
+ case 'json':
1493
+ return 'object';
1494
+ default:
1495
+ return type;
1496
+ }
1497
+ },
1498
+
1499
+ /**
1500
+ * Refer to https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#dataTypes
1501
+ * @param {String} type
1502
+ * @returns {String}
1503
+ */
1504
+ getFormat: type => {
1505
+ switch (type) {
1506
+ case 'date':
1507
+ return 'date';
1508
+ case 'datetime':
1509
+ return 'date-time';
1510
+ case 'password':
1511
+ return 'password';
1512
+ default:
1513
+ return undefined;
1514
+ }
1515
+ },
1516
+
1517
+ getPluginDefaultVerbDocumentation: function(pluginName, docName, routePath, verb) {
1518
+ try {
1519
+ const documentation = JSON.parse(
1520
+ fs.readFileSync(this.getPluginOverrideDocumentationPath(pluginName, docName), 'utf8')
1521
+ );
1522
+
1523
+ return _.get(documentation, ['paths', routePath, verb], null);
1524
+ } catch (err) {
1525
+ return null;
1526
+ }
1527
+ },
1528
+
1529
+ getPluginDefaultTagsDocumentation: function(pluginName, docName) {
1530
+ try {
1531
+ const documentation = JSON.parse(
1532
+ fs.readFileSync(this.getPluginOverrideDocumentationPath(pluginName, docName), 'utf8')
1533
+ );
1534
+
1535
+ return _.get(documentation, ['tags'], null);
1536
+ } catch (err) {
1537
+ return null;
1538
+ }
1539
+ },
1540
+
1541
+ getPluginOverrideComponents: function(pluginName, docName) {
1542
+ try {
1543
+ const documentation = JSON.parse(
1544
+ fs.readFileSync(this.getPluginOverrideDocumentationPath(pluginName, docName), 'utf8')
1545
+ );
1546
+
1547
+ return _.get(documentation, 'components', null);
1548
+ } catch (err) {
1549
+ return null;
1550
+ }
1551
+ },
1552
+
1553
+ getPluginOverrideDocumentationPath: function(pluginName, docName) {
1554
+ const defaultPath = path.join(
1555
+ strapi.config.appPath,
1556
+ 'extensions',
1557
+ pluginName,
1558
+ 'documentation',
1559
+ this.getDocumentationVersion(),
1560
+ 'overrides'
1561
+ );
1562
+
1563
+ if (docName) {
1564
+ return path.resolve(defaultPath, `${docName.json}`);
1565
+ } else {
1566
+ return defaultPath;
1567
+ }
1568
+ },
1569
+
1570
+ /**
1571
+ * Given a plugin retrieve its documentation version
1572
+ */
1573
+ getPluginDocumentationPath: function(pluginName) {
1574
+ return path.join(
1575
+ strapi.config.appPath,
1576
+ 'extensions',
1577
+ pluginName,
1578
+ 'documentation',
1579
+ this.getDocumentationVersion()
1580
+ );
1581
+ },
1582
+
1583
+ /**
1584
+ * Retrieve all plugins that have a description inside one of its route
1585
+ * @return {Arrray}
1586
+ */
1587
+ getPluginsWithDocumentationNeeded: function() {
1588
+ return Object.keys(strapi.plugins).reduce((acc, current) => {
1589
+ const isDocumentationNeeded = this.isPluginDocumentationNeeded(current);
1590
+
1591
+ if (isDocumentationNeeded) {
1592
+ return acc.concat(current);
1593
+ }
1594
+
1595
+ return acc;
1596
+ }, []);
1597
+ },
1598
+
1599
+ /**
1600
+ * Retrieve all the routes that have a description from a plugin
1601
+ * @param {String} pluginName
1602
+ * @returns {Array}
1603
+ */
1604
+ getPluginRoutesWithDescription: function(pluginName) {
1605
+ return _.get(strapi, ['plugins', pluginName, 'config', 'routes'], []).filter(
1606
+ route => _.get(route, ['config', 'description']) !== undefined
1607
+ );
1608
+ },
1609
+
1610
+ /**
1611
+ * Given a string and a pluginName retrieve the model and the pluginName
1612
+ * @param {String} string
1613
+ * @param {Sting} pluginName
1614
+ * @returns {Array}
1615
+ */
1616
+ getModelAndNameForPlugin: (string, pluginName) => {
1617
+ return _.replace(string, `${pluginName}-`, `${pluginName}.`).split('.');
1618
+ },
1619
+
1620
+ /**
1621
+ * Retrieve the path needed to get a model from a plugin
1622
+ * @param (String) string
1623
+ * @param {String} plugin
1624
+ * @returns {Array}
1625
+ */
1626
+ getModelForPlugin: function(string, pluginName) {
1627
+ const [plugin, model] = this.getModelAndNameForPlugin(string, pluginName);
1628
+
1629
+ return ['plugins', plugin, 'models', _.lowerCase(model)];
1630
+ },
1631
+
1632
+ /**
1633
+ * Check whether or not a plugin needs documentation
1634
+ * @param {String} pluginName
1635
+ * @returns {Boolean}
1636
+ */
1637
+ isPluginDocumentationNeeded: function(pluginName) {
1638
+ const { pluginsForWhichToGenerateDoc } = strapi.plugins.documentation.config['x-strapi-config'];
1639
+ if (
1640
+ Array.isArray(pluginsForWhichToGenerateDoc) &&
1641
+ !pluginsForWhichToGenerateDoc.includes(pluginName)
1642
+ ) {
1643
+ return false;
1644
+ } else {
1645
+ return this.getPluginRoutesWithDescription(pluginName).length > 0;
1646
+ }
1647
+ },
1648
+
1649
+ /**
1650
+ * Merge two components by replacing the default ones by the overides and keeping the others
1651
+ * @param {Object} initObj
1652
+ * @param {Object} srcObj
1653
+ * @returns {Object}
1654
+ */
1655
+ mergeComponents: (initObj, srcObj) => {
1656
+ const cleanedObj = Object.keys(_.get(initObj, 'schemas', {})).reduce((acc, current) => {
1657
+ const targetObj = _.has(_.get(srcObj, ['schemas'], {}), current) ? srcObj : initObj;
1658
+
1659
+ _.set(acc, ['schemas', current], _.get(targetObj, ['schemas', current], {}));
1660
+
1661
+ return acc;
1662
+ }, {});
1663
+
1664
+ return _.merge(cleanedObj, srcObj);
1665
+ },
1666
+
1667
+ mergePaths: function(initObj, srcObj) {
1668
+ return Object.keys(initObj.paths).reduce((acc, current) => {
1669
+ if (_.has(_.get(srcObj, ['paths'], {}), current)) {
1670
+ const verbs = Object.keys(initObj.paths[current]).reduce((acc1, curr) => {
1671
+ const verb = this.mergeVerbObject(
1672
+ initObj.paths[current][curr],
1673
+ _.get(srcObj, ['paths', current, curr], {})
1674
+ );
1675
+ _.set(acc1, [curr], verb);
1676
+
1677
+ return acc1;
1678
+ }, {});
1679
+ _.set(acc, ['paths', current], verbs);
1680
+ } else {
1681
+ _.set(acc, ['paths', current], _.get(initObj, ['paths', current], {}));
1682
+ }
1683
+
1684
+ return acc;
1685
+ }, {});
1686
+ },
1687
+
1688
+ mergeTags: (initObj, srcObj) => {
1689
+ return _.get(srcObj, 'tags', _.get(initObj, 'tags', []));
1690
+ },
1691
+
1692
+ /**
1693
+ * Merge two verb objects with a customizer
1694
+ * @param {Object} initObj
1695
+ * @param {Object} srcObj
1696
+ * @returns {Object}
1697
+ */
1698
+ mergeVerbObject: function(initObj, srcObj) {
1699
+ return _.mergeWith(initObj, srcObj, (objValue, srcValue) => {
1700
+ if (_.isPlainObject(objValue)) {
1701
+ return Object.assign(objValue, srcValue);
1702
+ }
1703
+
1704
+ return srcValue;
1705
+ });
1706
+ },
1707
+
1708
+ retrieveDocumentation: function(name, isPlugin = false) {
1709
+ const documentationPath = isPlugin
1710
+ ? [strapi.config.appPath, 'extensions', name, 'documentation', this.getDocumentationVersion()]
1711
+ : [strapi.config.appPath, 'api', name, 'documentation', this.getDocumentationVersion()];
1712
+
1713
+ try {
1714
+ const documentationFiles = fs
1715
+ .readdirSync(path.resolve(documentationPath.join('/')))
1716
+ .filter(el => el.includes('.json'));
1717
+
1718
+ return documentationFiles.reduce((acc, current) => {
1719
+ try {
1720
+ const doc = JSON.parse(
1721
+ fs.readFileSync(path.resolve([...documentationPath, current].join('/')), 'utf8')
1722
+ );
1723
+ acc.push(doc);
1724
+ } catch (err) {
1725
+ // console.log(path.resolve([...documentationPath, current].join('/')), err);
1726
+ }
1727
+
1728
+ return acc;
1729
+ }, []);
1730
+ } catch (err) {
1731
+ return [];
1732
+ }
1733
+ },
1734
+
1735
+ /**
1736
+ * Retrieve all documentation files from either the APIs or the plugins
1737
+ * @param {Boolean} isPlugin
1738
+ * @returns {Array}
1739
+ */
1740
+ retrieveDocumentationFiles: function(isPlugin = false, version = this.getDocumentationVersion()) {
1741
+ const array = isPlugin ? this.getPluginsWithDocumentationNeeded() : this.getApis();
1742
+
1743
+ return array.reduce((acc, current) => {
1744
+ const documentationPath = isPlugin
1745
+ ? [strapi.config.appPath, 'extensions', current, 'documentation', version]
1746
+ : [strapi.config.appPath, 'api', current, 'documentation', version];
1747
+
1748
+ try {
1749
+ const documentationFiles = fs
1750
+ .readdirSync(path.resolve(documentationPath.join('/')))
1751
+ .filter(el => el.includes('.json'));
1752
+
1753
+ documentationFiles.forEach(el => {
1754
+ try {
1755
+ let documentation = JSON.parse(
1756
+ fs.readFileSync(path.resolve([...documentationPath, el].join('/')), 'utf8')
1757
+ );
1758
+ /* eslint-disable indent */
1759
+ const overrideDocumentationPath = isPlugin
1760
+ ? path.resolve(
1761
+ strapi.config.appPath,
1762
+ 'extensions',
1763
+ current,
1764
+ 'documentation',
1765
+ version,
1766
+ 'overrides',
1767
+ el
1768
+ )
1769
+ : path.resolve(
1770
+ strapi.config.appPath,
1771
+ 'api',
1772
+ current,
1773
+ 'documentation',
1774
+ version,
1775
+ 'overrides',
1776
+ el
1777
+ );
1778
+ /* eslint-enable indent */
1779
+ let overrideDocumentation;
1780
+
1781
+ try {
1782
+ overrideDocumentation = JSON.parse(
1783
+ fs.readFileSync(overrideDocumentationPath, 'utf8')
1784
+ );
1785
+ } catch (err) {
1786
+ overrideDocumentation = null;
1787
+ }
1788
+
1789
+ if (!_.isEmpty(overrideDocumentation)) {
1790
+ documentation.paths = this.mergePaths(documentation, overrideDocumentation).paths;
1791
+ documentation.tags = _.cloneDeep(
1792
+ this.mergeTags(documentation, overrideDocumentation)
1793
+ );
1794
+ const documentationComponents = _.get(documentation, 'components', {});
1795
+ const overrideComponents = _.get(overrideDocumentation, 'components', {});
1796
+ const mergedComponents = this.mergeComponents(
1797
+ documentationComponents,
1798
+ overrideComponents
1799
+ );
1800
+
1801
+ if (!_.isEmpty(mergedComponents)) {
1802
+ documentation.components = mergedComponents;
1803
+ }
1804
+ }
1805
+
1806
+ acc.push(documentation);
1807
+ } catch (err) {
1808
+ strapi.log.error(err);
1809
+ console.log(
1810
+ `Unable to access the documentation for ${[...documentationPath, el].join('/')}`
1811
+ );
1812
+ }
1813
+ });
1814
+ } catch (err) {
1815
+ strapi.log.error(err);
1816
+ console.log(
1817
+ `Unable to retrieve documentation for the ${isPlugin ? 'plugin' : 'api'} ${current}`
1818
+ );
1819
+ }
1820
+
1821
+ return acc;
1822
+ }, []);
1823
+ },
1824
+
1825
+ retrieveDocumentationVersions: function() {
1826
+ return fs
1827
+ .readdirSync(this.getFullDocumentationPath())
1828
+ .map(version => {
1829
+ try {
1830
+ const doc = JSON.parse(
1831
+ fs.readFileSync(
1832
+ path.resolve(this.getFullDocumentationPath(), version, 'full_documentation.json')
1833
+ )
1834
+ );
1835
+ const generatedDate = _.get(doc, ['info', 'x-generation-date'], null);
1836
+
1837
+ return { version, generatedDate, url: '' };
1838
+ } catch (err) {
1839
+ return null;
1840
+ }
1841
+ })
1842
+ .filter(x => x);
1843
+ },
1844
+
1845
+ retrieveFrontForm: async function() {
1846
+ const config = await strapi
1847
+ .store({
1848
+ environment: '',
1849
+ type: 'plugin',
1850
+ name: 'documentation',
1851
+ key: 'config',
1852
+ })
1853
+ .get();
1854
+ const forms = JSON.parse(JSON.stringify(form));
1855
+
1856
+ _.set(forms, [0, 0, 'value'], config.restrictedAccess);
1857
+ _.set(forms, [0, 1, 'value'], config.password || '');
1858
+
1859
+ return forms;
1860
+ },
1861
+ };