@strapi/plugin-documentation 0.0.0-4fc90398602f
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/LICENSE +22 -0
- package/README.md +151 -0
- package/__mocks__/strapi.js +41 -0
- package/__tests__/build-component-schema.test.js +271 -0
- package/admin/src/components/FieldActionWrapper/index.js +14 -0
- package/admin/src/components/PluginIcon/index.js +12 -0
- package/admin/src/index.js +80 -0
- 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/utils/api.js +31 -0
- package/admin/src/pages/utils/schema.js +11 -0
- package/admin/src/pages/utils/useReactQuery.js +46 -0
- package/admin/src/permissions.js +19 -0
- package/admin/src/pluginId.js +5 -0
- package/admin/src/translations/ar.json +20 -0
- package/admin/src/translations/cs.json +21 -0
- package/admin/src/translations/de.json +26 -0
- package/admin/src/translations/dk.json +39 -0
- package/admin/src/translations/en.json +39 -0
- package/admin/src/translations/es.json +39 -0
- package/admin/src/translations/fr.json +26 -0
- package/admin/src/translations/id.json +24 -0
- package/admin/src/translations/it.json +26 -0
- package/admin/src/translations/ko.json +39 -0
- package/admin/src/translations/ms.json +23 -0
- package/admin/src/translations/nl.json +21 -0
- package/admin/src/translations/pl.json +39 -0
- package/admin/src/translations/pt-BR.json +21 -0
- package/admin/src/translations/pt.json +21 -0
- package/admin/src/translations/ru.json +28 -0
- package/admin/src/translations/sk.json +24 -0
- package/admin/src/translations/th.json +24 -0
- package/admin/src/translations/tr.json +20 -0
- package/admin/src/translations/uk.json +23 -0
- package/admin/src/translations/vi.json +24 -0
- package/admin/src/translations/zh-Hans.json +28 -0
- package/admin/src/translations/zh.json +24 -0
- package/admin/src/utils/getTrad.js +5 -0
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/openWithNewTab.js +20 -0
- package/package.json +62 -0
- package/server/bootstrap.js +54 -0
- package/server/config/default-plugin-config.js +74 -0
- package/server/config/index.js +7 -0
- package/server/controllers/documentation.js +241 -0
- package/server/controllers/index.js +7 -0
- package/server/index.js +17 -0
- package/server/middlewares/documentation.js +25 -0
- package/server/middlewares/index.js +7 -0
- package/server/middlewares/restrict-access.js +24 -0
- package/server/public/index.html +70 -0
- package/server/public/login.html +145 -0
- package/server/register.js +11 -0
- package/server/routes/index.js +84 -0
- package/server/services/documentation.js +209 -0
- package/server/services/helpers/build-api-endpoint-path.js +185 -0
- package/server/services/helpers/build-component-schema.js +159 -0
- package/server/services/helpers/index.js +9 -0
- package/server/services/helpers/utils/clean-schema-attributes.js +212 -0
- package/server/services/helpers/utils/get-api-responses.js +105 -0
- package/server/services/helpers/utils/get-schema-data.js +32 -0
- package/server/services/helpers/utils/loop-content-type-names.js +53 -0
- package/server/services/helpers/utils/pascal-case.js +9 -0
- package/server/services/helpers/utils/query-params.js +84 -0
- package/server/services/helpers/utils/routes.js +10 -0
- package/server/services/index.js +7 -0
- package/strapi-admin.js +3 -0
- package/strapi-server.js +3 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!-- HTML for static distribution bundle build --><!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Swagger UI</title>
|
|
6
|
+
<link
|
|
7
|
+
rel="stylesheet"
|
|
8
|
+
type="text/css"
|
|
9
|
+
href="<%=backendUrl%>/plugins/documentation/swagger-ui.css"
|
|
10
|
+
/>
|
|
11
|
+
<link
|
|
12
|
+
rel="icon"
|
|
13
|
+
type="image/png"
|
|
14
|
+
href="<%=backendUrl%>/plugins/documentation/favicon-32x32.png"
|
|
15
|
+
sizes="32x32"
|
|
16
|
+
/>
|
|
17
|
+
<link
|
|
18
|
+
rel="icon"
|
|
19
|
+
type="image/png"
|
|
20
|
+
href="<%=backendUrl%>/plugins/documentation/favicon-16x16.png"
|
|
21
|
+
sizes="16x16"
|
|
22
|
+
/>
|
|
23
|
+
<style>
|
|
24
|
+
html {
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
overflow: -moz-scrollbars-vertical;
|
|
27
|
+
overflow-y: scroll;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
*,
|
|
31
|
+
*:before,
|
|
32
|
+
*:after {
|
|
33
|
+
box-sizing: inherit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
body {
|
|
37
|
+
margin: 0;
|
|
38
|
+
background: #fafafa;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
|
|
43
|
+
<body>
|
|
44
|
+
<div id="swagger-ui"></div>
|
|
45
|
+
<script class="custom-swagger-ui">
|
|
46
|
+
window.onload = function() {
|
|
47
|
+
const ui = SwaggerUIBundle({
|
|
48
|
+
url: "https://petstore.swagger.io/v2/swagger.json",
|
|
49
|
+
spec: <%=spec%>,
|
|
50
|
+
dom_id: '#swagger-ui',
|
|
51
|
+
docExpansion: "none",
|
|
52
|
+
deepLinking: true,
|
|
53
|
+
presets: [
|
|
54
|
+
SwaggerUIBundle.presets.apis,
|
|
55
|
+
SwaggerUIStandalonePreset,
|
|
56
|
+
],
|
|
57
|
+
plugins: [
|
|
58
|
+
SwaggerUIBundle.plugins.DownloadUrl,
|
|
59
|
+
],
|
|
60
|
+
layout: "StandaloneLayout",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
window.ui = ui;
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<script src="<%=backendUrl%>/plugins/documentation/swagger-ui-bundle.js"></script>
|
|
68
|
+
<script src="<%=backendUrl%>/plugins/documentation/swagger-ui-standalone-preset.js"></script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Login - Documentation</title>
|
|
5
|
+
<link href="https://fonts.googleapis.com/css?family=Lato:400,700" rel="stylesheet" />
|
|
6
|
+
<style>
|
|
7
|
+
html {
|
|
8
|
+
font-size: 62.5%;
|
|
9
|
+
height: 100%;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
height: 100%;
|
|
16
|
+
margin: 0;
|
|
17
|
+
background-color: #ffffff;
|
|
18
|
+
font-family: 'Lato';
|
|
19
|
+
font-size: 1.4rem;
|
|
20
|
+
font-weight: 400;
|
|
21
|
+
text-rendering: optimizeLegibility;
|
|
22
|
+
-webkit-font-smoothing: antialiased;
|
|
23
|
+
-moz-osx-font-smoothing: grayscale;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.login {
|
|
27
|
+
height: 100%;
|
|
28
|
+
background-color: #f6f9fc;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.login .login-form {
|
|
32
|
+
height: calc(100% - 70px);
|
|
33
|
+
padding: 68px 0 0;
|
|
34
|
+
text-align: center;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.login .login-form form {
|
|
38
|
+
position: relative;
|
|
39
|
+
max-width: 460px;
|
|
40
|
+
padding: 26px 30px;
|
|
41
|
+
margin: 55px auto 0;
|
|
42
|
+
background-color: #ffffff;
|
|
43
|
+
border-radius: 3px;
|
|
44
|
+
box-shadow: 0px 2px 4px rgba(91, 107, 174, 0.15);
|
|
45
|
+
text-align: center;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.login .login-form form:before {
|
|
49
|
+
position: absolute;
|
|
50
|
+
content: '';
|
|
51
|
+
top: 0px;
|
|
52
|
+
left: 0;
|
|
53
|
+
display: inline-block;
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 2px;
|
|
56
|
+
background-color: #2b66cc;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.login .login-form form .error {
|
|
60
|
+
display: block;
|
|
61
|
+
color: #ff4e00;
|
|
62
|
+
padding-bottom: 20px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.login .login-form .sub-title {
|
|
66
|
+
margin-top: 35px;
|
|
67
|
+
font-size: 1.6rem;
|
|
68
|
+
font-weight: 400;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.login .login-form .logo {
|
|
72
|
+
max-height: 40px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.login .login-form form label {
|
|
76
|
+
display: block;
|
|
77
|
+
margin-bottom: 18px;
|
|
78
|
+
width: 100%;
|
|
79
|
+
text-align: left;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.login .login-form form input {
|
|
84
|
+
outline: none;
|
|
85
|
+
width: calc(100% - 30px);
|
|
86
|
+
height: 36px;
|
|
87
|
+
padding: 0 15px;
|
|
88
|
+
border: 1px solid #ececec;
|
|
89
|
+
border-radius: 2px;
|
|
90
|
+
margin-bottom: 20px;
|
|
91
|
+
line-height: 36px;
|
|
92
|
+
text-align: left;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.login .login-form form input[type='submit'] {
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
display: inline-block;
|
|
98
|
+
width: auto;
|
|
99
|
+
margin: 12px auto 0;
|
|
100
|
+
padding: 0 75px;
|
|
101
|
+
background: transparent;
|
|
102
|
+
border-radius: 36px;
|
|
103
|
+
border: 1px solid #2b66cc;
|
|
104
|
+
color: #2b66cc;
|
|
105
|
+
text-transform: uppercase;
|
|
106
|
+
font-size: 1.4rem;
|
|
107
|
+
font-weight: 700;
|
|
108
|
+
transition: all 0.2s ease-out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.login .login-form form input[type='submit']:hover {
|
|
112
|
+
background: #2b66cc;
|
|
113
|
+
color: #ffffff;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
116
|
+
</head>
|
|
117
|
+
<body>
|
|
118
|
+
<div class="login">
|
|
119
|
+
<section class="login-form">
|
|
120
|
+
<div class="container">
|
|
121
|
+
<div class="row">
|
|
122
|
+
<div class="col-lg-6 col-lg-offset-3 col-md-12">
|
|
123
|
+
<img
|
|
124
|
+
alt="Strapi logo"
|
|
125
|
+
class="logo"
|
|
126
|
+
src="https://strapi.io/assets/images/logo_login.png"
|
|
127
|
+
/>
|
|
128
|
+
<h2 class="sub-title">Enter the password to access the documentation.</h2>
|
|
129
|
+
<form method="post" action="<%=actionUrl%>">
|
|
130
|
+
<span class="error">Wrong password...</span>
|
|
131
|
+
<label>Password</label>
|
|
132
|
+
<input
|
|
133
|
+
type="password"
|
|
134
|
+
name="password"
|
|
135
|
+
placeholder="•••••••••"
|
|
136
|
+
/>
|
|
137
|
+
<input type="submit" value="Login" />
|
|
138
|
+
</form>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</section>
|
|
143
|
+
</div>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const registerDocumentationMiddleWare = require('./middlewares/documentation');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Register upload plugin
|
|
7
|
+
* @param {{ strapi: import('@strapi/strapi').Strapi }}
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ strapi }) => {
|
|
10
|
+
await registerDocumentationMiddleWare({ strapi });
|
|
11
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const restrictAccess = require('../middlewares/restrict-access');
|
|
4
|
+
|
|
5
|
+
module.exports = [
|
|
6
|
+
{
|
|
7
|
+
method: 'GET',
|
|
8
|
+
path: '/',
|
|
9
|
+
handler: 'documentation.index',
|
|
10
|
+
config: {
|
|
11
|
+
auth: false,
|
|
12
|
+
middlewares: [restrictAccess],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
method: 'GET',
|
|
17
|
+
path: '/v:major(\\d+).:minor(\\d+).:patch(\\d+)',
|
|
18
|
+
handler: 'documentation.index',
|
|
19
|
+
config: {
|
|
20
|
+
auth: false,
|
|
21
|
+
middlewares: [restrictAccess],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
method: 'GET',
|
|
26
|
+
path: '/login',
|
|
27
|
+
handler: 'documentation.loginView',
|
|
28
|
+
config: {
|
|
29
|
+
auth: false,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
method: 'POST',
|
|
34
|
+
path: '/login',
|
|
35
|
+
handler: 'documentation.login',
|
|
36
|
+
config: {
|
|
37
|
+
auth: false,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
method: 'GET',
|
|
42
|
+
path: '/getInfos',
|
|
43
|
+
handler: 'documentation.getInfos',
|
|
44
|
+
config: {
|
|
45
|
+
policies: [
|
|
46
|
+
{ name: 'admin::hasPermissions', config: { actions: ['plugin::documentation.read'] } },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
method: 'POST',
|
|
52
|
+
path: '/regenerateDoc',
|
|
53
|
+
handler: 'documentation.regenerateDoc',
|
|
54
|
+
config: {
|
|
55
|
+
policies: [
|
|
56
|
+
{
|
|
57
|
+
name: 'admin::hasPermissions',
|
|
58
|
+
config: { actions: ['plugin::documentation.settings.regenerate'] },
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
method: 'PUT',
|
|
65
|
+
path: '/updateSettings',
|
|
66
|
+
handler: 'documentation.updateSettings',
|
|
67
|
+
config: {
|
|
68
|
+
policies: [
|
|
69
|
+
{
|
|
70
|
+
name: 'admin::hasPermissions',
|
|
71
|
+
config: { actions: ['plugin::documentation.settings.update'] },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
method: 'DELETE',
|
|
78
|
+
path: '/deleteDoc/:version',
|
|
79
|
+
handler: 'documentation.deleteDoc',
|
|
80
|
+
config: {
|
|
81
|
+
policies: [],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const _ = require('lodash');
|
|
6
|
+
const { getAbsoluteServerUrl } = require('@strapi/utils');
|
|
7
|
+
|
|
8
|
+
const defaultPluginConfig = require('../config/default-plugin-config');
|
|
9
|
+
const { builApiEndpointPath, buildComponentSchema } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
module.exports = ({ strapi }) => {
|
|
12
|
+
const config = strapi.config.get('plugin.documentation');
|
|
13
|
+
|
|
14
|
+
const registeredDocs = [];
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
registerDoc(doc) {
|
|
18
|
+
// parseYaml
|
|
19
|
+
if (typeof doc === 'string') {
|
|
20
|
+
doc = require('yaml').parse(doc);
|
|
21
|
+
}
|
|
22
|
+
// receive an object we can register it directly
|
|
23
|
+
registeredDocs.push(doc);
|
|
24
|
+
},
|
|
25
|
+
getDocumentationVersion() {
|
|
26
|
+
return _.get(config, 'info.version');
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
getFullDocumentationPath() {
|
|
30
|
+
return path.join(strapi.dirs.app.extensions, 'documentation', 'documentation');
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
getCustomDocumentationPath() {
|
|
34
|
+
// ??
|
|
35
|
+
return path.join(strapi.dirs.app.extensions, 'documentation', 'config', 'settings.json');
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
getDocumentationVersions() {
|
|
39
|
+
return fs
|
|
40
|
+
.readdirSync(this.getFullDocumentationPath())
|
|
41
|
+
.map((version) => {
|
|
42
|
+
try {
|
|
43
|
+
const doc = JSON.parse(
|
|
44
|
+
fs.readFileSync(
|
|
45
|
+
path.resolve(this.getFullDocumentationPath(), version, 'full_documentation.json')
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
const generatedDate = _.get(doc, ['info', 'x-generation-date'], null);
|
|
49
|
+
|
|
50
|
+
return { version, generatedDate, url: '' };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.filter((x) => x);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns settings stored in core-store
|
|
60
|
+
*/
|
|
61
|
+
async getDocumentationAccess() {
|
|
62
|
+
const { restrictedAccess } = await strapi
|
|
63
|
+
.store({
|
|
64
|
+
environment: '',
|
|
65
|
+
type: 'plugin',
|
|
66
|
+
name: 'documentation',
|
|
67
|
+
key: 'config',
|
|
68
|
+
})
|
|
69
|
+
.get();
|
|
70
|
+
|
|
71
|
+
return { restrictedAccess };
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @description - Gets the path for an api or plugin
|
|
76
|
+
*
|
|
77
|
+
* @param {object} api
|
|
78
|
+
* @property {string} api.name - Name of the api
|
|
79
|
+
* @property {string} api.getter - api | plugin
|
|
80
|
+
*
|
|
81
|
+
* @returns path to the api | plugin
|
|
82
|
+
*/
|
|
83
|
+
getApiDocumentationPath(api) {
|
|
84
|
+
if (api.getter === 'plugin') {
|
|
85
|
+
return path.join(strapi.dirs.app.extensions, api.name, 'documentation');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return path.join(strapi.dirs.app.api, api.name, 'documentation');
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async deleteDocumentation(version) {
|
|
92
|
+
const apis = this.getPluginAndApiInfo();
|
|
93
|
+
for (const api of apis) {
|
|
94
|
+
await fs.remove(path.join(this.getApiDocumentationPath(api), version));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await fs.remove(path.join(this.getFullDocumentationPath(), version));
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
getPluginAndApiInfo() {
|
|
101
|
+
const plugins = _.get(config, 'x-strapi-config.plugins');
|
|
102
|
+
const pluginsToDocument = plugins.map((plugin) => {
|
|
103
|
+
return {
|
|
104
|
+
name: plugin,
|
|
105
|
+
getter: 'plugin',
|
|
106
|
+
ctNames: Object.keys(strapi.plugin(plugin).contentTypes),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const apisToDocument = Object.keys(strapi.api).map((api) => {
|
|
111
|
+
return {
|
|
112
|
+
name: api,
|
|
113
|
+
getter: 'api',
|
|
114
|
+
ctNames: Object.keys(strapi.api[api].contentTypes),
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return [...apisToDocument, ...pluginsToDocument];
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async getCustomConfig() {
|
|
122
|
+
const customConfigPath = this.getCustomDocumentationPath();
|
|
123
|
+
const pathExists = await fs.pathExists(customConfigPath);
|
|
124
|
+
if (pathExists) {
|
|
125
|
+
return fs.readJson(customConfigPath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {};
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @description - Creates the Swagger json files
|
|
133
|
+
*/
|
|
134
|
+
async generateFullDoc(version = this.getDocumentationVersion()) {
|
|
135
|
+
let paths = {};
|
|
136
|
+
let schemas = {};
|
|
137
|
+
const apis = this.getPluginAndApiInfo();
|
|
138
|
+
for (const api of apis) {
|
|
139
|
+
const apiName = api.name;
|
|
140
|
+
const apiDirPath = path.join(this.getApiDocumentationPath(api), version);
|
|
141
|
+
|
|
142
|
+
const apiDocPath = path.join(apiDirPath, `${apiName}.json`);
|
|
143
|
+
|
|
144
|
+
const apiPath = builApiEndpointPath(api);
|
|
145
|
+
|
|
146
|
+
if (!apiPath) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await fs.ensureFile(apiDocPath);
|
|
151
|
+
await fs.writeJson(apiDocPath, apiPath, { spaces: 2 });
|
|
152
|
+
|
|
153
|
+
const componentSchema = buildComponentSchema(api);
|
|
154
|
+
|
|
155
|
+
schemas = {
|
|
156
|
+
...schemas,
|
|
157
|
+
...componentSchema,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
paths = { ...paths, ...apiPath };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const fullDocJsonPath = path.join(
|
|
164
|
+
this.getFullDocumentationPath(),
|
|
165
|
+
version,
|
|
166
|
+
'full_documentation.json'
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const defaultConfig = _.cloneDeep(defaultPluginConfig);
|
|
170
|
+
|
|
171
|
+
const serverUrl = getAbsoluteServerUrl(strapi.config);
|
|
172
|
+
const apiPath = strapi.config.get('api.rest.prefix');
|
|
173
|
+
|
|
174
|
+
_.set(defaultConfig, 'servers', [
|
|
175
|
+
{
|
|
176
|
+
url: `${serverUrl}${apiPath}`,
|
|
177
|
+
description: 'Development server',
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
_.set(defaultConfig, ['info', 'x-generation-date'], new Date().toISOString());
|
|
181
|
+
_.set(defaultConfig, ['info', 'version'], version);
|
|
182
|
+
_.merge(defaultConfig.components, { schemas });
|
|
183
|
+
|
|
184
|
+
const customConfig = await this.getCustomConfig();
|
|
185
|
+
const config = _.merge(defaultConfig, customConfig);
|
|
186
|
+
|
|
187
|
+
const finalDoc = { ...config, paths };
|
|
188
|
+
|
|
189
|
+
registeredDocs.forEach((doc) => {
|
|
190
|
+
// Add tags
|
|
191
|
+
finalDoc.tags = finalDoc.tags || [];
|
|
192
|
+
finalDoc.tags.push(...(doc.tags || []));
|
|
193
|
+
|
|
194
|
+
// Add Paths
|
|
195
|
+
_.assign(finalDoc.paths, doc.paths);
|
|
196
|
+
|
|
197
|
+
// Add components
|
|
198
|
+
_.forEach(doc.components || {}, (val, key) => {
|
|
199
|
+
finalDoc.components[key] = finalDoc.components[key] || {};
|
|
200
|
+
|
|
201
|
+
_.assign(finalDoc.components[key], val);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await fs.ensureFile(fullDocJsonPath);
|
|
206
|
+
await fs.writeJson(fullDocJsonPath, finalDoc, { spaces: 2 });
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const pathToRegexp = require('path-to-regexp');
|
|
5
|
+
|
|
6
|
+
const pascalCase = require('./utils/pascal-case');
|
|
7
|
+
const queryParams = require('./utils/query-params');
|
|
8
|
+
const loopContentTypeNames = require('./utils/loop-content-type-names');
|
|
9
|
+
const getApiResponses = require('./utils/get-api-responses');
|
|
10
|
+
const { hasFindMethod, isLocalizedPath } = require('./utils/routes');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @description Parses a route with ':variable'
|
|
14
|
+
*
|
|
15
|
+
* @param {string} routePath - The route's path property
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
const parsePathWithVariables = (routePath) => {
|
|
19
|
+
return pathToRegexp
|
|
20
|
+
.parse(routePath)
|
|
21
|
+
.map((token) => {
|
|
22
|
+
if (_.isObject(token)) {
|
|
23
|
+
return `${token.prefix}{${token.name}}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return token;
|
|
27
|
+
})
|
|
28
|
+
.join('');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @description Builds the required object for a path parameter
|
|
33
|
+
*
|
|
34
|
+
* @param {string} routePath - The route's path property
|
|
35
|
+
*
|
|
36
|
+
* @returns {object } Swagger path params object
|
|
37
|
+
*/
|
|
38
|
+
const getPathParams = (routePath) => {
|
|
39
|
+
return pathToRegexp
|
|
40
|
+
.parse(routePath)
|
|
41
|
+
.filter((token) => _.isObject(token))
|
|
42
|
+
.map((param) => {
|
|
43
|
+
return {
|
|
44
|
+
name: param.name,
|
|
45
|
+
in: 'path',
|
|
46
|
+
description: '',
|
|
47
|
+
deprecated: false,
|
|
48
|
+
required: true,
|
|
49
|
+
schema: { type: 'string' },
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
*
|
|
56
|
+
* @param {string} prefix - The prefix found on the routes object
|
|
57
|
+
* @param {string} route - The current route
|
|
58
|
+
* @property {string} route.path - The current route's path
|
|
59
|
+
* @property {object} route.config - The current route's config object
|
|
60
|
+
*
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
const getPathWithPrefix = (prefix, route) => {
|
|
64
|
+
// When the prefix is set on the routes and
|
|
65
|
+
// the current route is not trying to remove it
|
|
66
|
+
if (prefix && !_.has(route.config, 'prefix')) {
|
|
67
|
+
// Add the prefix to the path
|
|
68
|
+
return prefix.concat(route.path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Otherwise just return path
|
|
72
|
+
return route.path;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* @description Gets all paths based on routes
|
|
76
|
+
*
|
|
77
|
+
* @param {object} apiInfo
|
|
78
|
+
* @property {object} apiInfo.routeInfo - The api routes object
|
|
79
|
+
* @property {string} apiInfo.uniqueName - Content type name | Api name + Content type name
|
|
80
|
+
* @property {object} apiInfo.contentTypeInfo - The info object found on content type schemas
|
|
81
|
+
*
|
|
82
|
+
* @returns {object}
|
|
83
|
+
*/
|
|
84
|
+
const getPaths = ({ routeInfo, uniqueName, contentTypeInfo }) => {
|
|
85
|
+
// Get the routes for the current content type
|
|
86
|
+
const contentTypeRoutes = routeInfo.routes.filter((route) => {
|
|
87
|
+
return (
|
|
88
|
+
route.path.includes(contentTypeInfo.pluralName) ||
|
|
89
|
+
route.path.includes(contentTypeInfo.singularName)
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const paths = contentTypeRoutes.reduce((acc, route) => {
|
|
94
|
+
// TODO: Find a more reliable way to determine list of entities vs a single entity
|
|
95
|
+
const isListOfEntities = hasFindMethod(route.handler);
|
|
96
|
+
const isLocalizationPath = isLocalizedPath(route.path);
|
|
97
|
+
const methodVerb = route.method.toLowerCase();
|
|
98
|
+
const hasPathParams = route.path.includes('/:');
|
|
99
|
+
const pathWithPrefix = getPathWithPrefix(routeInfo.prefix, route);
|
|
100
|
+
const routePath = hasPathParams ? parsePathWithVariables(pathWithPrefix) : pathWithPrefix;
|
|
101
|
+
const { responses } = getApiResponses({
|
|
102
|
+
uniqueName,
|
|
103
|
+
route,
|
|
104
|
+
isListOfEntities,
|
|
105
|
+
isLocalizationPath,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const swaggerConfig = {
|
|
109
|
+
responses,
|
|
110
|
+
tags: [_.upperFirst(uniqueName)],
|
|
111
|
+
parameters: [],
|
|
112
|
+
operationId: `${methodVerb}${routePath}`,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (isListOfEntities) {
|
|
116
|
+
swaggerConfig.parameters.push(...queryParams);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (hasPathParams) {
|
|
120
|
+
const pathParams = getPathParams(route.path);
|
|
121
|
+
swaggerConfig.parameters.push(...pathParams);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (['post', 'put'].includes(methodVerb)) {
|
|
125
|
+
const refName = isLocalizationPath ? 'LocalizationRequest' : 'Request';
|
|
126
|
+
const requestBody = {
|
|
127
|
+
required: true,
|
|
128
|
+
content: {
|
|
129
|
+
'application/json': {
|
|
130
|
+
schema: {
|
|
131
|
+
$ref: `#/components/schemas/${pascalCase(uniqueName)}${refName}`,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
swaggerConfig.requestBody = requestBody;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_.set(acc, `${routePath}.${methodVerb}`, swaggerConfig);
|
|
141
|
+
|
|
142
|
+
return acc;
|
|
143
|
+
}, {});
|
|
144
|
+
|
|
145
|
+
return paths;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @decription Gets all open api paths object for a given content type
|
|
150
|
+
*
|
|
151
|
+
* @param {object} apiInfo
|
|
152
|
+
*
|
|
153
|
+
* @returns {object} Open API paths
|
|
154
|
+
*/
|
|
155
|
+
const getAllPathsForContentType = (apiInfo) => {
|
|
156
|
+
let paths = {};
|
|
157
|
+
|
|
158
|
+
const pathsObject = getPaths(apiInfo);
|
|
159
|
+
|
|
160
|
+
paths = {
|
|
161
|
+
...paths,
|
|
162
|
+
...pathsObject,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return paths;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @description - Builds the Swagger paths object for each api
|
|
170
|
+
*
|
|
171
|
+
* @param {object} api - Information about the current api
|
|
172
|
+
* @property {string} api.name - The name of the api
|
|
173
|
+
* @property {string} api.getter - The getter for the api (api | plugin)
|
|
174
|
+
* @property {array} api.ctNames - The name of all contentTypes found on the api
|
|
175
|
+
*
|
|
176
|
+
* @returns {object}
|
|
177
|
+
*/
|
|
178
|
+
const buildApiEndpointPath = (api) => {
|
|
179
|
+
// A reusable loop for building paths and component schemas
|
|
180
|
+
// Uses the api param to build a new set of params for each content type
|
|
181
|
+
// Passes these new params to the function provided
|
|
182
|
+
return loopContentTypeNames(api, getAllPathsForContentType);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
module.exports = buildApiEndpointPath;
|