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