@strapi/plugin-documentation 4.0.0-next.9 → 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.
- package/admin/src/components/FieldActionWrapper/index.js +14 -0
- package/admin/src/components/PluginIcon/index.js +12 -0
- package/admin/src/index.js +23 -11
- package/admin/src/pages/PluginPage/index.js +199 -0
- package/admin/src/pages/PluginPage/tests/index.test.js +873 -0
- package/admin/src/pages/PluginPage/tests/server.js +23 -0
- package/admin/src/pages/SettingsPage/index.js +181 -0
- package/admin/src/pages/SettingsPage/tests/index.test.js +612 -0
- package/admin/src/pages/SettingsPage/tests/server.js +18 -0
- package/admin/src/pages/{HomePage/utils → utils}/api.js +5 -4
- package/admin/src/pages/{HomePage/utils → utils}/schema.js +0 -0
- package/admin/src/pages/utils/useReactQuery.js +46 -0
- package/admin/src/translations/ar.json +0 -3
- package/admin/src/translations/cs.json +0 -3
- package/admin/src/translations/de.json +0 -3
- package/admin/src/translations/en.json +14 -3
- package/admin/src/translations/es.json +0 -3
- package/admin/src/translations/fr.json +0 -3
- package/admin/src/translations/id.json +0 -3
- package/admin/src/translations/it.json +0 -3
- package/admin/src/translations/ko.json +0 -3
- package/admin/src/translations/ms.json +0 -3
- package/admin/src/translations/nl.json +0 -3
- package/admin/src/translations/pl.json +0 -3
- package/admin/src/translations/pt-BR.json +0 -3
- package/admin/src/translations/pt.json +0 -3
- package/admin/src/translations/ru.json +0 -3
- package/admin/src/translations/sk.json +0 -3
- package/admin/src/translations/th.json +0 -3
- package/admin/src/translations/tr.json +0 -3
- package/admin/src/translations/uk.json +0 -3
- package/admin/src/translations/vi.json +0 -3
- package/admin/src/translations/zh-Hans.json +3 -6
- package/admin/src/translations/zh.json +0 -3
- package/package.json +32 -47
- package/server/bootstrap.js +19 -105
- package/server/config/default-config.js +12 -15
- package/server/config/index.js +10 -2
- package/server/controllers/documentation.js +61 -127
- package/server/index.js +17 -0
- package/server/middlewares/documentation.js +18 -41
- package/server/{policies/index-policy.js → middlewares/restrict-access.js} +5 -16
- package/{public → server/public}/index.html +0 -0
- package/{public → server/public}/login.html +0 -0
- package/server/register.js +11 -0
- package/server/routes/index.js +18 -25
- package/server/services/documentation.js +125 -1835
- package/server/utils/builders/build-api-endpoint-path.js +174 -0
- package/server/utils/builders/build-api-requests.js +41 -0
- package/server/utils/builders/build-api-responses.js +108 -0
- package/server/utils/builders/index.js +11 -0
- package/server/utils/clean-schema-attributes.js +205 -0
- package/server/utils/error-response.js +22 -0
- package/server/utils/get-schema-data.js +32 -0
- package/server/utils/query-params.js +84 -0
- package/strapi-admin.js +3 -0
- package/strapi-server.js +1 -19
- package/admin/src/assets/images/logo.svg +0 -1
- package/admin/src/components/Block/components.js +0 -26
- package/admin/src/components/Block/index.js +0 -39
- package/admin/src/components/Copy/index.js +0 -36
- package/admin/src/components/Header/index.js +0 -72
- package/admin/src/components/Row/ButtonContainer.js +0 -67
- package/admin/src/components/Row/components.js +0 -83
- package/admin/src/components/Row/index.js +0 -51
- package/admin/src/pages/App/index.js +0 -21
- package/admin/src/pages/HomePage/components.js +0 -59
- package/admin/src/pages/HomePage/index.js +0 -175
- package/admin/src/pages/HomePage/useHomePage.js +0 -56
- package/server/policies/index.js +0 -7
- package/server/services/utils/forms.json +0 -29
|
@@ -1,1865 +1,155 @@
|
|
|
1
1
|
'use strict';
|
|
2
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
3
|
const path = require('path');
|
|
4
|
+
const fs = require('fs-extra');
|
|
10
5
|
const _ = require('lodash');
|
|
11
|
-
const
|
|
12
|
-
const pathToRegexp = require('path-to-regexp');
|
|
13
|
-
// FIXME
|
|
14
|
-
/* eslint-disable import/extensions */
|
|
15
|
-
const defaultSettings = require('../config/default-config');
|
|
16
|
-
const defaultComponents = require('./utils/components.json');
|
|
17
|
-
const form = require('./utils/forms.json');
|
|
18
|
-
const parametersOptions = require('./utils/parametersOptions.json');
|
|
6
|
+
const { getAbsoluteServerUrl } = require('@strapi/utils');
|
|
19
7
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const customIsEqual = (obj1, obj2) => _.isEqualWith(obj1, obj2, customComparator);
|
|
8
|
+
const { builApiEndpointPath } = require('../utils/builders');
|
|
9
|
+
const defaultConfig = require('../config/default-config');
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
|
|
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 = ({ strapi }) => ({
|
|
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
|
-
},
|
|
11
|
+
module.exports = ({ strapi }) => {
|
|
12
|
+
const config = strapi.config.get('plugin.documentation');
|
|
215
13
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
14
|
+
return {
|
|
15
|
+
getDocumentationVersion() {
|
|
16
|
+
return _.get(config, 'info.version');
|
|
17
|
+
},
|
|
220
18
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
const arrayOfPromises = [];
|
|
226
|
-
fs.accessSync(folderPath);
|
|
227
|
-
const items = fs.readdirSync(folderPath).filter(x => x[0] !== '.');
|
|
19
|
+
getFullDocumentationPath() {
|
|
20
|
+
return path.join(strapi.dirs.extensions, 'documentation', 'documentation');
|
|
21
|
+
},
|
|
228
22
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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;
|
|
23
|
+
getDocumentationVersions() {
|
|
24
|
+
return fs
|
|
25
|
+
.readdirSync(this.getFullDocumentationPath())
|
|
26
|
+
.map(version => {
|
|
27
|
+
try {
|
|
28
|
+
const doc = JSON.parse(
|
|
29
|
+
fs.readFileSync(
|
|
30
|
+
path.resolve(this.getFullDocumentationPath(), version, 'full_documentation.json')
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
const generatedDate = _.get(doc, ['info', 'x-generation-date'], null);
|
|
360
34
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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' };
|
|
35
|
+
return { version, generatedDate, url: '' };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return null;
|
|
373
38
|
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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;
|
|
39
|
+
})
|
|
40
|
+
.filter(x => x);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns settings stored in core-store
|
|
45
|
+
*/
|
|
46
|
+
async getDocumentationAccess() {
|
|
47
|
+
const { restrictedAccess } = await strapi
|
|
48
|
+
.store({
|
|
49
|
+
environment: '',
|
|
50
|
+
type: 'plugin',
|
|
51
|
+
name: 'documentation',
|
|
52
|
+
key: 'config',
|
|
53
|
+
})
|
|
54
|
+
.get();
|
|
55
|
+
|
|
56
|
+
return { restrictedAccess };
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @description - Gets the path for an api or plugin
|
|
61
|
+
*
|
|
62
|
+
* @param {object} api
|
|
63
|
+
* @property {string} api.name - Name of the api
|
|
64
|
+
* @property {string} api.getter - api | plugin
|
|
65
|
+
*
|
|
66
|
+
* @returns path to the api | plugin
|
|
67
|
+
*/
|
|
68
|
+
getApiDocumentationPath(api) {
|
|
69
|
+
if (api.getter === 'plugin') {
|
|
70
|
+
return path.join(strapi.dirs.extensions, api.name, 'documentation');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return path.join(strapi.dirs.api, api.name, 'documentation');
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async deleteDocumentation(version) {
|
|
77
|
+
const apis = this.getPluginAndApiInfo();
|
|
78
|
+
for (const api of apis) {
|
|
79
|
+
await fs.remove(path.join(this.getApiDocumentationPath(api), version));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await fs.remove(path.join(this.getFullDocumentationPath(), version));
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
getPluginAndApiInfo() {
|
|
86
|
+
const plugins = _.get(config, 'x-strapi-config.plugins');
|
|
87
|
+
const pluginsToDocument = plugins.map(plugin => {
|
|
88
|
+
return {
|
|
89
|
+
name: plugin,
|
|
90
|
+
getter: 'plugin',
|
|
91
|
+
ctNames: Object.keys(strapi.plugin(plugin).contentTypes),
|
|
92
|
+
};
|
|
519
93
|
});
|
|
520
94
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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;
|
|
95
|
+
const apisToDocument = Object.keys(strapi.api).map(api => {
|
|
96
|
+
return {
|
|
97
|
+
name: api,
|
|
98
|
+
getter: 'api',
|
|
99
|
+
ctNames: Object.keys(strapi.api[api].contentTypes),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
591
102
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
strapi.components[component].associations
|
|
595
|
-
);
|
|
103
|
+
return [...apisToDocument, ...pluginsToDocument];
|
|
104
|
+
},
|
|
596
105
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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;
|
|
106
|
+
/**
|
|
107
|
+
* @description - Creates the Swagger json files
|
|
108
|
+
*/
|
|
109
|
+
async generateFullDoc(version = this.getDocumentationVersion()) {
|
|
110
|
+
let paths = {};
|
|
616
111
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
);
|
|
112
|
+
const apis = this.getPluginAndApiInfo();
|
|
113
|
+
for (const api of apis) {
|
|
114
|
+
const apiName = api.name;
|
|
115
|
+
const apiDirPath = path.join(this.getApiDocumentationPath(api), version);
|
|
622
116
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
properties: {
|
|
626
|
-
__component: {
|
|
627
|
-
type: 'string',
|
|
628
|
-
enum: components,
|
|
629
|
-
},
|
|
630
|
-
},
|
|
631
|
-
},
|
|
632
|
-
schema
|
|
633
|
-
);
|
|
634
|
-
});
|
|
117
|
+
const apiDocPath = path.join(apiDirPath, `${apiName}.json`);
|
|
118
|
+
const apiPathsObject = builApiEndpointPath(api);
|
|
635
119
|
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
};
|
|
120
|
+
if (!apiPathsObject) {
|
|
121
|
+
continue;
|
|
656
122
|
}
|
|
657
123
|
|
|
658
|
-
|
|
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;
|
|
124
|
+
await fs.ensureFile(apiDocPath);
|
|
125
|
+
await fs.writeJson(apiDocPath, apiPathsObject, { spaces: 2 });
|
|
684
126
|
|
|
685
|
-
|
|
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'];
|
|
127
|
+
paths = { ...paths, ...apiPathsObject.paths };
|
|
693
128
|
}
|
|
694
129
|
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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}`
|
|
130
|
+
const fullDocJsonPath = path.join(
|
|
131
|
+
this.getFullDocumentationPath(),
|
|
132
|
+
version,
|
|
133
|
+
'full_documentation.json'
|
|
713
134
|
);
|
|
714
135
|
|
|
715
|
-
|
|
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.config.get('plugin.documentation.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.config.get('plugin.documentation.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
|
-
}
|
|
136
|
+
const settings = _.cloneDeep(defaultConfig);
|
|
1164
137
|
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
}
|
|
138
|
+
const serverUrl = getAbsoluteServerUrl(strapi.config);
|
|
139
|
+
const apiPath = strapi.config.get('api.rest.prefix');
|
|
1200
140
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
141
|
+
_.set(settings, 'servers', [
|
|
142
|
+
{
|
|
143
|
+
url: `${serverUrl}${apiPath}`,
|
|
144
|
+
description: 'Development server',
|
|
1205
145
|
},
|
|
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
|
-
},
|
|
146
|
+
]);
|
|
1408
147
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
},
|
|
148
|
+
_.set(settings, ['info', 'x-generation-date'], new Date().toISOString());
|
|
149
|
+
_.set(settings, ['info', 'version'], version);
|
|
1412
150
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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.config.get(
|
|
1641
|
-
'plugins.documentation.x-strapi-config'
|
|
1642
|
-
);
|
|
1643
|
-
if (
|
|
1644
|
-
Array.isArray(pluginsForWhichToGenerateDoc) &&
|
|
1645
|
-
!pluginsForWhichToGenerateDoc.includes(pluginName)
|
|
1646
|
-
) {
|
|
1647
|
-
return false;
|
|
1648
|
-
} else {
|
|
1649
|
-
return this.getPluginRoutesWithDescription(pluginName).length > 0;
|
|
1650
|
-
}
|
|
1651
|
-
},
|
|
1652
|
-
|
|
1653
|
-
/**
|
|
1654
|
-
* Merge two components by replacing the default ones by the overides and keeping the others
|
|
1655
|
-
* @param {Object} initObj
|
|
1656
|
-
* @param {Object} srcObj
|
|
1657
|
-
* @returns {Object}
|
|
1658
|
-
*/
|
|
1659
|
-
mergeComponents: (initObj, srcObj) => {
|
|
1660
|
-
const cleanedObj = Object.keys(_.get(initObj, 'schemas', {})).reduce((acc, current) => {
|
|
1661
|
-
const targetObj = _.has(_.get(srcObj, ['schemas'], {}), current) ? srcObj : initObj;
|
|
1662
|
-
|
|
1663
|
-
_.set(acc, ['schemas', current], _.get(targetObj, ['schemas', current], {}));
|
|
1664
|
-
|
|
1665
|
-
return acc;
|
|
1666
|
-
}, {});
|
|
1667
|
-
|
|
1668
|
-
return _.merge(cleanedObj, srcObj);
|
|
1669
|
-
},
|
|
1670
|
-
|
|
1671
|
-
mergePaths: function(initObj, srcObj) {
|
|
1672
|
-
return Object.keys(initObj.paths).reduce((acc, current) => {
|
|
1673
|
-
if (_.has(_.get(srcObj, ['paths'], {}), current)) {
|
|
1674
|
-
const verbs = Object.keys(initObj.paths[current]).reduce((acc1, curr) => {
|
|
1675
|
-
const verb = this.mergeVerbObject(
|
|
1676
|
-
initObj.paths[current][curr],
|
|
1677
|
-
_.get(srcObj, ['paths', current, curr], {})
|
|
1678
|
-
);
|
|
1679
|
-
_.set(acc1, [curr], verb);
|
|
1680
|
-
|
|
1681
|
-
return acc1;
|
|
1682
|
-
}, {});
|
|
1683
|
-
_.set(acc, ['paths', current], verbs);
|
|
1684
|
-
} else {
|
|
1685
|
-
_.set(acc, ['paths', current], _.get(initObj, ['paths', current], {}));
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
return acc;
|
|
1689
|
-
}, {});
|
|
1690
|
-
},
|
|
1691
|
-
|
|
1692
|
-
mergeTags: (initObj, srcObj) => {
|
|
1693
|
-
return _.get(srcObj, 'tags', _.get(initObj, 'tags', []));
|
|
1694
|
-
},
|
|
1695
|
-
|
|
1696
|
-
/**
|
|
1697
|
-
* Merge two verb objects with a customizer
|
|
1698
|
-
* @param {Object} initObj
|
|
1699
|
-
* @param {Object} srcObj
|
|
1700
|
-
* @returns {Object}
|
|
1701
|
-
*/
|
|
1702
|
-
mergeVerbObject: function(initObj, srcObj) {
|
|
1703
|
-
return _.mergeWith(initObj, srcObj, (objValue, srcValue) => {
|
|
1704
|
-
if (_.isPlainObject(objValue)) {
|
|
1705
|
-
return Object.assign(objValue, srcValue);
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
return srcValue;
|
|
1709
|
-
});
|
|
1710
|
-
},
|
|
1711
|
-
|
|
1712
|
-
retrieveDocumentation: function(name, isPlugin = false) {
|
|
1713
|
-
const documentationPath = isPlugin
|
|
1714
|
-
? [strapi.config.appPath, 'extensions', name, 'documentation', this.getDocumentationVersion()]
|
|
1715
|
-
: [strapi.config.appPath, 'api', name, 'documentation', this.getDocumentationVersion()];
|
|
1716
|
-
|
|
1717
|
-
try {
|
|
1718
|
-
const documentationFiles = fs
|
|
1719
|
-
.readdirSync(path.resolve(documentationPath.join('/')))
|
|
1720
|
-
.filter(el => el.includes('.json'));
|
|
1721
|
-
|
|
1722
|
-
return documentationFiles.reduce((acc, current) => {
|
|
1723
|
-
try {
|
|
1724
|
-
const doc = JSON.parse(
|
|
1725
|
-
fs.readFileSync(path.resolve([...documentationPath, current].join('/')), 'utf8')
|
|
1726
|
-
);
|
|
1727
|
-
acc.push(doc);
|
|
1728
|
-
} catch (err) {
|
|
1729
|
-
// console.log(path.resolve([...documentationPath, current].join('/')), err);
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
return acc;
|
|
1733
|
-
}, []);
|
|
1734
|
-
} catch (err) {
|
|
1735
|
-
return [];
|
|
1736
|
-
}
|
|
1737
|
-
},
|
|
1738
|
-
|
|
1739
|
-
/**
|
|
1740
|
-
* Retrieve all documentation files from either the APIs or the plugins
|
|
1741
|
-
* @param {Boolean} isPlugin
|
|
1742
|
-
* @returns {Array}
|
|
1743
|
-
*/
|
|
1744
|
-
retrieveDocumentationFiles: function(isPlugin = false, version = this.getDocumentationVersion()) {
|
|
1745
|
-
const array = isPlugin ? this.getPluginsWithDocumentationNeeded() : this.getApis();
|
|
1746
|
-
|
|
1747
|
-
return array.reduce((acc, current) => {
|
|
1748
|
-
const documentationPath = isPlugin
|
|
1749
|
-
? [strapi.config.appPath, 'extensions', current, 'documentation', version]
|
|
1750
|
-
: [strapi.config.appPath, 'api', current, 'documentation', version];
|
|
1751
|
-
|
|
1752
|
-
try {
|
|
1753
|
-
const documentationFiles = fs
|
|
1754
|
-
.readdirSync(path.resolve(documentationPath.join('/')))
|
|
1755
|
-
.filter(el => el.includes('.json'));
|
|
1756
|
-
|
|
1757
|
-
documentationFiles.forEach(el => {
|
|
1758
|
-
try {
|
|
1759
|
-
let documentation = JSON.parse(
|
|
1760
|
-
fs.readFileSync(path.resolve([...documentationPath, el].join('/')), 'utf8')
|
|
1761
|
-
);
|
|
1762
|
-
/* eslint-disable indent */
|
|
1763
|
-
const overrideDocumentationPath = isPlugin
|
|
1764
|
-
? path.resolve(
|
|
1765
|
-
strapi.config.appPath,
|
|
1766
|
-
'extensions',
|
|
1767
|
-
current,
|
|
1768
|
-
'documentation',
|
|
1769
|
-
version,
|
|
1770
|
-
'overrides',
|
|
1771
|
-
el
|
|
1772
|
-
)
|
|
1773
|
-
: path.resolve(
|
|
1774
|
-
strapi.config.appPath,
|
|
1775
|
-
'api',
|
|
1776
|
-
current,
|
|
1777
|
-
'documentation',
|
|
1778
|
-
version,
|
|
1779
|
-
'overrides',
|
|
1780
|
-
el
|
|
1781
|
-
);
|
|
1782
|
-
/* eslint-enable indent */
|
|
1783
|
-
let overrideDocumentation;
|
|
1784
|
-
|
|
1785
|
-
try {
|
|
1786
|
-
overrideDocumentation = JSON.parse(
|
|
1787
|
-
fs.readFileSync(overrideDocumentationPath, 'utf8')
|
|
1788
|
-
);
|
|
1789
|
-
} catch (err) {
|
|
1790
|
-
overrideDocumentation = null;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
if (!_.isEmpty(overrideDocumentation)) {
|
|
1794
|
-
documentation.paths = this.mergePaths(documentation, overrideDocumentation).paths;
|
|
1795
|
-
documentation.tags = _.cloneDeep(
|
|
1796
|
-
this.mergeTags(documentation, overrideDocumentation)
|
|
1797
|
-
);
|
|
1798
|
-
const documentationComponents = _.get(documentation, 'components', {});
|
|
1799
|
-
const overrideComponents = _.get(overrideDocumentation, 'components', {});
|
|
1800
|
-
const mergedComponents = this.mergeComponents(
|
|
1801
|
-
documentationComponents,
|
|
1802
|
-
overrideComponents
|
|
1803
|
-
);
|
|
1804
|
-
|
|
1805
|
-
if (!_.isEmpty(mergedComponents)) {
|
|
1806
|
-
documentation.components = mergedComponents;
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
acc.push(documentation);
|
|
1811
|
-
} catch (err) {
|
|
1812
|
-
strapi.log.error(err);
|
|
1813
|
-
console.log(
|
|
1814
|
-
`Unable to access the documentation for ${[...documentationPath, el].join('/')}`
|
|
1815
|
-
);
|
|
1816
|
-
}
|
|
1817
|
-
});
|
|
1818
|
-
} catch (err) {
|
|
1819
|
-
strapi.log.error(err);
|
|
1820
|
-
console.log(
|
|
1821
|
-
`Unable to retrieve documentation for the ${isPlugin ? 'plugin' : 'api'} ${current}`
|
|
1822
|
-
);
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
return acc;
|
|
1826
|
-
}, []);
|
|
1827
|
-
},
|
|
1828
|
-
|
|
1829
|
-
retrieveDocumentationVersions: function() {
|
|
1830
|
-
return fs
|
|
1831
|
-
.readdirSync(this.getFullDocumentationPath())
|
|
1832
|
-
.map(version => {
|
|
1833
|
-
try {
|
|
1834
|
-
const doc = JSON.parse(
|
|
1835
|
-
fs.readFileSync(
|
|
1836
|
-
path.resolve(this.getFullDocumentationPath(), version, 'full_documentation.json')
|
|
1837
|
-
)
|
|
1838
|
-
);
|
|
1839
|
-
const generatedDate = _.get(doc, ['info', 'x-generation-date'], null);
|
|
1840
|
-
|
|
1841
|
-
return { version, generatedDate, url: '' };
|
|
1842
|
-
} catch (err) {
|
|
1843
|
-
return null;
|
|
1844
|
-
}
|
|
1845
|
-
})
|
|
1846
|
-
.filter(x => x);
|
|
1847
|
-
},
|
|
1848
|
-
|
|
1849
|
-
retrieveFrontForm: async function() {
|
|
1850
|
-
const config = await strapi
|
|
1851
|
-
.store({
|
|
1852
|
-
environment: '',
|
|
1853
|
-
type: 'plugin',
|
|
1854
|
-
name: 'documentation',
|
|
1855
|
-
key: 'config',
|
|
1856
|
-
})
|
|
1857
|
-
.get();
|
|
1858
|
-
const forms = JSON.parse(JSON.stringify(form));
|
|
1859
|
-
|
|
1860
|
-
_.set(forms, [0, 0, 'value'], config.restrictedAccess);
|
|
1861
|
-
_.set(forms, [0, 1, 'value'], config.password || '');
|
|
1862
|
-
|
|
1863
|
-
return forms;
|
|
1864
|
-
},
|
|
1865
|
-
});
|
|
151
|
+
await fs.ensureFile(fullDocJsonPath);
|
|
152
|
+
await fs.writeJson(fullDocJsonPath, { ...settings, paths }, { spaces: 2 });
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|