Skip to content

Commit cdbeace

Browse files
Merge pull request #35 from wednesday-solutions/feat/swag
feat: swag
2 parents bf0a203 + 8cf96b3 commit cdbeace

File tree

6 files changed

+285
-8
lines changed

6 files changed

+285
-8
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,15 @@
6060
"lodash": "^4.17.21",
6161
"moment": "^2.29.1",
6262
"mongoose": "6.2.4",
63+
"mongoose-to-swagger": "^1.4.0",
6364
"node-fetch": "2",
6465
"nodemon": "^2.0.15",
6566
"opossum": "^6.3.0",
67+
"pluralize": "^8.0.0",
6668
"response-time": "^2.3.2",
6769
"save": "^2.4.0",
68-
"slack-notify": "^2.0.2"
70+
"slack-notify": "^2.0.2",
71+
"swagger-ui-express": "^4.3.0"
6972
},
7073
"devDependencies": {
7174
"@babel/cli": "^7.16.7",

server/api/index.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import fs from 'fs';
21
import path from 'path';
32
import express from 'express';
43
import kebab from 'lodash/kebabCase';
@@ -10,10 +9,10 @@ import {
109
generateFetchOneRequest
1110
} from 'api/requestGenerators';
1211
import { mongoConnector } from 'middlewares/mongo';
13-
1412
import { customApisMapper, REQUEST_TYPES } from 'api/customApisMapper';
15-
import customRoutes from '../routes';
16-
import { isTestEnv } from 'utils';
13+
import customRoutes from 'server/routes/index';
14+
import { getModelFiles, isTestEnv } from 'utils';
15+
import { registerSwagger } from 'utils/swagUtils';
1716

1817
/* istanbul ignore next */
1918
if (!isTestEnv()) {
@@ -24,20 +23,20 @@ export default app => {
2423
autoGenerateApisFromModels(app);
2524
// Custom api
2625
app.use('/', customRoutes);
26+
registerSwagger(app);
2727
};
2828

2929
const autoGenerateApisFromModels = app => {
3030
const modelsFolderPath = path.join(__dirname, '../../models/');
31-
const fileArray = fs
32-
.readdirSync(modelsFolderPath)
33-
.filter(file => fs.lstatSync(modelsFolderPath + file).isFile());
31+
const fileArray = getModelFiles(modelsFolderPath);
3432
fileArray.forEach(f => {
3533
const { model } = require(modelsFolderPath + f);
3634
const name = f.split('.')[0];
3735

3836
apiGeneratorFactory(app, name, model);
3937
});
4038
};
39+
4140
const apiGeneratorFactory = (app, name, model) => {
4241
const router = express.Router();
4342
Object.values(REQUEST_TYPES).forEach(type => {

server/utils/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,13 @@
1+
import fs from 'fs';
2+
13
export const isTestEnv = () =>
24
process.env.ENVIRONMENT_NAME === 'test' || process.env.NODE_ENV === 'test';
5+
6+
export const getModelFiles = modelsFolderPath => {
7+
if (typeof modelsFolderPath !== 'string') {
8+
throw new Error('modelPathString is invalid');
9+
}
10+
return fs
11+
.readdirSync(modelsFolderPath)
12+
.filter(file => fs.lstatSync(modelsFolderPath + file).isFile());
13+
};

server/utils/swagUtils.js

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import path from 'path';
2+
import pluralize from 'pluralize';
3+
import m2s from 'mongoose-to-swagger';
4+
import kebabCase from 'lodash/kebabCase';
5+
import swaggerUi from 'swagger-ui-express';
6+
import { REQUEST_TYPES } from 'api/customApisMapper';
7+
import Pack from '../../package.json';
8+
import { getModelFiles } from '.';
9+
import customSwaggerDoc from '../../swagger.json';
10+
11+
export const REQUEST_METHODS = {
12+
[REQUEST_TYPES.create]: 'post',
13+
[REQUEST_TYPES.update]: 'patch',
14+
[REQUEST_TYPES.fetchOne]: 'get',
15+
[REQUEST_TYPES.fetchAll]: 'get',
16+
[REQUEST_TYPES.remove]: 'delete'
17+
};
18+
19+
export const DEFAULT_DEFINITIONS = {
20+
deleteResponse: {
21+
type: 'object',
22+
properties: {
23+
deletedCount: {
24+
type: 'integer',
25+
format: 'int64',
26+
example: 1
27+
}
28+
}
29+
}
30+
};
31+
32+
/**
33+
* @typedef CustomSwagger
34+
* @type {object}
35+
* @property {Array|object} tags
36+
* @property {object} paths
37+
* @property {object} definitions
38+
*/
39+
40+
/**
41+
*
42+
* @param {any} app
43+
* @param {CustomSwagger} customSwagger
44+
*/
45+
export const registerSwagger = app => {
46+
const SWAGGER_DOCS_PATH = '/api-docs/swagger.json';
47+
const options = {
48+
swaggerOptions: {
49+
url: SWAGGER_DOCS_PATH
50+
}
51+
};
52+
const swaggerDocument = generateSwaggerDoc();
53+
appendToSwaggerDoc(swaggerDocument, customSwaggerDoc);
54+
app.get(SWAGGER_DOCS_PATH, (_, res) => res.json(swaggerDocument));
55+
app.use(
56+
'/api-docs',
57+
swaggerUi.serveFiles(null, options),
58+
swaggerUi.setup(null, options)
59+
);
60+
};
61+
62+
export const generateSwaggerDoc = () => {
63+
const swaggerDocument = {
64+
swagger: '2.0',
65+
info: {
66+
title: 'Parcel Node Mongo Express Documentation',
67+
version: Pack.version
68+
},
69+
tags: [],
70+
paths: {},
71+
definitions: {}
72+
};
73+
const modelsFolderPath = path.join(__dirname, '../../models/');
74+
const fileArray = getModelFiles(modelsFolderPath);
75+
fileArray.forEach(f => {
76+
const { model } = require(modelsFolderPath + f);
77+
const name = f.split('.')[0];
78+
79+
const { swaggerPaths, swaggerDefs } = swagGeneratorFactory(name, model);
80+
appendToSwaggerDoc(swaggerDocument, {
81+
paths: swaggerPaths,
82+
definitions: swaggerDefs,
83+
tags: {
84+
name,
85+
description: `${name} related endpoints`
86+
}
87+
});
88+
});
89+
return swaggerDocument;
90+
};
91+
92+
/**
93+
*
94+
* @param {any} swaggerDocument
95+
* @param {CustomSwagger} swaggerData
96+
*/
97+
export const appendToSwaggerDoc = (swaggerDocument, swaggerData) => {
98+
const { paths, definitions, tags } = swaggerData;
99+
if (Array.isArray(tags)) {
100+
swaggerDocument.tags.push(...tags);
101+
} else {
102+
swaggerDocument.tags.push(tags);
103+
}
104+
swaggerDocument.paths = {
105+
...swaggerDocument.paths,
106+
...paths
107+
};
108+
swaggerDocument.definitions = {
109+
...swaggerDocument.definitions,
110+
...definitions
111+
};
112+
};
113+
114+
export const swagGeneratorFactory = (name, model) => {
115+
const swaggerPaths = {};
116+
const swaggerDefs = {
117+
...DEFAULT_DEFINITIONS
118+
};
119+
appendSwagDefs(name, model, swaggerDefs);
120+
Object.values(REQUEST_TYPES).forEach(type =>
121+
appendSwagPaths(type, name, swaggerPaths)
122+
);
123+
return { swaggerPaths, swaggerDefs };
124+
};
125+
126+
export const appendSwagPaths = (type, name, swaggerPaths) => {
127+
if (type === REQUEST_TYPES.create && name === 'orders') {
128+
return;
129+
}
130+
const routeName = `/${kebabCase(name)}`;
131+
const method = REQUEST_METHODS[type];
132+
const lowerType = type.toLowerCase();
133+
const isPluralEnity = type === REQUEST_TYPES.fetchAll;
134+
const hasPathParam = ![
135+
REQUEST_TYPES.create,
136+
REQUEST_TYPES.fetchAll
137+
].includes(type);
138+
const entityName = isPluralEnity ? name : pluralize.singular(name);
139+
const summary = `${lowerType} ${entityName}`;
140+
const parameters = hasPathParam
141+
? [
142+
{
143+
name: '_id',
144+
in: 'path',
145+
description: `ID of ${pluralize.singular(
146+
name
147+
)} to ${lowerType}`,
148+
required: true,
149+
type: 'string'
150+
}
151+
]
152+
: {};
153+
const responses = {
154+
200: {
155+
type: 'object',
156+
description: `${lowerType} ${entityName} is success`,
157+
schema: {
158+
type: 'object',
159+
properties: {
160+
data: isPluralEnity
161+
? {
162+
type: 'array',
163+
items: { $ref: `#/definitions/${name}` }
164+
}
165+
: type === REQUEST_TYPES.remove
166+
? { $ref: '#/definitions/deleteResponse' }
167+
: { $ref: `#/definitions/${name}` }
168+
}
169+
}
170+
},
171+
400: {
172+
type: 'object',
173+
description: `${lowerType} ${entityName} is failed`,
174+
schema: {
175+
type: 'object',
176+
required: ['error'],
177+
properties: {
178+
error: {
179+
type: 'string',
180+
example: `unable to ${lowerType} ${entityName}`
181+
}
182+
}
183+
}
184+
}
185+
};
186+
const pathKey = !hasPathParam ? routeName : `${routeName}/{_id}`;
187+
swaggerPaths[pathKey] = {
188+
...(swaggerPaths[pathKey] || {}),
189+
[method]: {
190+
tags: [name],
191+
summary,
192+
produces: ['application/json'],
193+
parameters,
194+
responses
195+
}
196+
};
197+
};
198+
199+
export const appendSwagDefs = (name, model, swaggerDefs) => {
200+
const modelSchema = m2s(model);
201+
// modify model schema properties here
202+
if (modelSchema.properties.purchasedProducts) {
203+
modelSchema.properties.purchasedProducts = {
204+
$ref: '#/definitions/products'
205+
};
206+
}
207+
swaggerDefs[name] = {
208+
type: 'object',
209+
...modelSchema,
210+
title: undefined
211+
};
212+
};

swagger.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"info": {
3+
"title": "Parcel Node Mongo Express Documentation",
4+
"version": "1.0.0"
5+
},
6+
"host": "http://localhost:9000",
7+
"basePath": "/",
8+
"swagger": "2.0",
9+
"tags": ["auth"],
10+
"paths": {
11+
"/login": {
12+
"post": {
13+
"tags": ["auth"],
14+
"descriptions": "Login user",
15+
"responses": {
16+
"200": {
17+
"description": "Successful login",
18+
"schema": {
19+
"type": "object"
20+
}
21+
}
22+
}
23+
}
24+
}
25+
},
26+
"definitions": {},
27+
"responses": {},
28+
"parameters": {},
29+
"securityDefinitions": {}
30+
}

yarn.lock

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5608,6 +5608,11 @@ mongodb@4.3.1:
56085608
optionalDependencies:
56095609
saslprep "^1.0.3"
56105610

5611+
mongoose-to-swagger@^1.4.0:
5612+
version "1.4.0"
5613+
resolved "https://registry.yarnpkg.com/mongoose-to-swagger/-/mongoose-to-swagger-1.4.0.tgz#30e8b9a9766277f8ec82dbcb69e424ed288168ef"
5614+
integrity sha512-7O5f2bSVT7euXgMMhlxe4gz6sJW8E3GWHties2FR3B+W41yhW5dpG4RuC7rGL3tKi9nyDN/nkorkbs5v0AHLyg==
5615+
56115616
mongoose@6.2.4:
56125617
version "6.2.4"
56135618
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-6.2.4.tgz#c6ff4f84ab47b6c760e773424b0fd1f392d98563"
@@ -6207,6 +6212,11 @@ please-upgrade-node@^3.1.1:
62076212
dependencies:
62086213
semver-compare "^1.0.0"
62096214

6215+
pluralize@^8.0.0:
6216+
version "8.0.0"
6217+
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
6218+
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
6219+
62106220
postcss-less@2.0.0:
62116221
version "2.0.0"
62126222
resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-2.0.0.tgz#5d190b8e057ca446d60fe2e2587ad791c9029fb8"
@@ -7377,6 +7387,18 @@ supports-preserve-symlinks-flag@^1.0.0:
73777387
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
73787388
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
73797389

7390+
swagger-ui-dist@>=4.1.3:
7391+
version "4.10.3"
7392+
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.10.3.tgz#d67066716ce20cb6afb23474c9ca2727633a9eb3"
7393+
integrity sha512-eR4vsd7sYo0Sx7ZKRP5Z04yij7JkNmIlUQfrDQgC+xO5ABYx+waabzN+nDsQTLAJ4Z04bjkRd8xqkJtbxr3G7w==
7394+
7395+
swagger-ui-express@^4.3.0:
7396+
version "4.3.0"
7397+
resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz#226238ab231f7718f9109d63a66efc3a795618dd"
7398+
integrity sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==
7399+
dependencies:
7400+
swagger-ui-dist ">=4.1.3"
7401+
73807402
swap-case@^1.1.0:
73817403
version "1.1.2"
73827404
resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-1.1.2.tgz#c39203a4587385fad3c850a0bd1bcafa081974e3"

0 commit comments

Comments
 (0)