@stanlemon/server 0.1.2 → 0.2.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/package.json +3 -2
- package/src/asyncJsonHandler.js +17 -9
- package/src/createAppServer.js +38 -26
- package/src/formatOutput.js +4 -2
- package/src/index.js +2 -1
- package/src/schemaHandler.js +44 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stanlemon/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A basic express web server setup.",
|
|
5
5
|
"author": "Stan Lemon <stanlemon@users.noreply.github.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"express-rate-limit": "^6.2.1",
|
|
23
23
|
"helmet": "^5.0.2",
|
|
24
24
|
"http-proxy-middleware": "^2.0.3",
|
|
25
|
+
"joi": "^17.6.0",
|
|
25
26
|
"lodash-es": "^4.17.21",
|
|
26
27
|
"morgan": "^1.10.0"
|
|
27
28
|
},
|
|
@@ -29,4 +30,4 @@
|
|
|
29
30
|
"@stanlemon/eslint-config": "0.1.4",
|
|
30
31
|
"nodemon": "^2.0.15"
|
|
31
32
|
}
|
|
32
|
-
}
|
|
33
|
+
}
|
package/src/asyncJsonHandler.js
CHANGED
|
@@ -26,22 +26,22 @@ export function asyncJsonHandler(fn) {
|
|
|
26
26
|
} catch (ex) {
|
|
27
27
|
// TODO: Add better support for validation errors
|
|
28
28
|
if (ex.message === "Bad Request") {
|
|
29
|
-
res.status(400).json({ error: ex
|
|
29
|
+
res.status(400).json({ error: formatError(ex) });
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
if (ex.message === "Not Authorized") {
|
|
34
|
-
res.status(403).json({ error: ex
|
|
34
|
+
res.status(403).json({ error: formatError(ex) });
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (ex.message === "Not Found") {
|
|
39
|
-
res.status(404).json({ error: ex
|
|
39
|
+
res.status(404).json({ error: formatError(ex) });
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
if (ex.message === "Already Exists") {
|
|
44
|
-
res.status(409).json({ error: ex
|
|
44
|
+
res.status(409).json({ error: formatError(ex) });
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -52,7 +52,7 @@ export function asyncJsonHandler(fn) {
|
|
|
52
52
|
// eslint-disable-next-line no-console
|
|
53
53
|
console.error(ex);
|
|
54
54
|
|
|
55
|
-
res.status(500).json({ error: ex
|
|
55
|
+
res.status(500).json({ error: formatError(ex) });
|
|
56
56
|
} else {
|
|
57
57
|
res.status(500).json({ error: "Something went wrong" });
|
|
58
58
|
}
|
|
@@ -60,14 +60,19 @@ export function asyncJsonHandler(fn) {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function formatError(ex) {
|
|
64
|
+
return ex.message + (ex.details ? ": " + ex.details : "");
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
export class BadRequestException extends Error {
|
|
64
68
|
static MESSAGE = "Bad Request";
|
|
65
69
|
static CODE = 400;
|
|
66
70
|
|
|
67
|
-
constructor() {
|
|
71
|
+
constructor(details = null) {
|
|
68
72
|
super(BadRequestException.MESSAGE);
|
|
69
73
|
this.name = "BadRequestException";
|
|
70
74
|
this.code = BadRequestException.CODE;
|
|
75
|
+
this.details = details;
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -75,10 +80,11 @@ export class NotAuthorizedException extends Error {
|
|
|
75
80
|
static MESSAGE = "Not Authorized";
|
|
76
81
|
static CODE = 403;
|
|
77
82
|
|
|
78
|
-
constructor() {
|
|
83
|
+
constructor(details = null) {
|
|
79
84
|
super(NotAuthorizedException.MESSAGE);
|
|
80
85
|
this.name = "NotAuthorizedException";
|
|
81
86
|
this.code = NotAuthorizedException.CODE;
|
|
87
|
+
this.details = details;
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -86,10 +92,11 @@ export class NotFoundException extends Error {
|
|
|
86
92
|
static MESSAGE = "Not Found";
|
|
87
93
|
static CODE = 404;
|
|
88
94
|
|
|
89
|
-
constructor() {
|
|
95
|
+
constructor(details = null) {
|
|
90
96
|
super(NotFoundException.MESSAGE);
|
|
91
97
|
this.name = "NotFoundException";
|
|
92
98
|
this.code = NotFoundException.CODE;
|
|
99
|
+
this.details = details;
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
|
|
@@ -97,9 +104,10 @@ export class AlreadyExistsException extends Error {
|
|
|
97
104
|
static MESSAGE = "Already Exists";
|
|
98
105
|
static CODE = 409;
|
|
99
106
|
|
|
100
|
-
constructor() {
|
|
107
|
+
constructor(details = null) {
|
|
101
108
|
super(AlreadyExistsException.MESSAGE);
|
|
102
109
|
this.name = "AlreadyExistsException";
|
|
103
110
|
this.code = AlreadyExistsException.CODE;
|
|
111
|
+
this.details = details;
|
|
104
112
|
}
|
|
105
113
|
}
|
package/src/createAppServer.js
CHANGED
|
@@ -8,39 +8,36 @@ import { createProxyMiddleware } from "http-proxy-middleware";
|
|
|
8
8
|
|
|
9
9
|
dotenv.config();
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
export const DEFAULTS = { port: 3000, webpack: false, start: true };
|
|
12
|
+
|
|
13
|
+
export default function createAppServer(options) {
|
|
14
|
+
const { port, webpack, start } = { ...DEFAULTS, ...options };
|
|
14
15
|
|
|
15
|
-
export default function createAppServer(opts = { port: 3000 }) {
|
|
16
16
|
const app = express();
|
|
17
17
|
app.use(express.json());
|
|
18
18
|
|
|
19
|
-
const limiter = rateLimit({
|
|
20
|
-
windowMs: 15 * 60 * 1000, // X minutes
|
|
21
|
-
max: 100, // limit each IP to Y requests per windowMs
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
app.use(limiter);
|
|
25
|
-
|
|
26
19
|
if (process.env.NODE_ENV !== "test") {
|
|
27
20
|
app.use(morgan("combined"));
|
|
28
21
|
}
|
|
29
22
|
|
|
30
23
|
if (process.env.NODE_ENV === "production") {
|
|
24
|
+
const limiter = rateLimit({
|
|
25
|
+
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
26
|
+
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
27
|
+
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
|
28
|
+
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
app.use(limiter);
|
|
31
32
|
app.use(compression());
|
|
32
33
|
app.use(helmet());
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
if (NODE_ENV !==
|
|
36
|
-
app.use(express.static("./dist"));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (NODE_ENV === DEVELOPMENT && WEBPACK_URL) {
|
|
36
|
+
if (webpack !== false && process.env.NODE_ENV !== "production") {
|
|
40
37
|
app.get(
|
|
41
38
|
"/*.js",
|
|
42
39
|
createProxyMiddleware({
|
|
43
|
-
target:
|
|
40
|
+
target: webpack,
|
|
44
41
|
changeOrigin: true,
|
|
45
42
|
})
|
|
46
43
|
);
|
|
@@ -48,10 +45,21 @@ export default function createAppServer(opts = { port: 3000 }) {
|
|
|
48
45
|
app.get(
|
|
49
46
|
"/",
|
|
50
47
|
createProxyMiddleware({
|
|
51
|
-
target:
|
|
48
|
+
target: webpack,
|
|
52
49
|
changeOrigin: true,
|
|
53
50
|
})
|
|
54
51
|
);
|
|
52
|
+
|
|
53
|
+
app.get(
|
|
54
|
+
"/ws",
|
|
55
|
+
createProxyMiddleware({
|
|
56
|
+
target: webpack,
|
|
57
|
+
changeOrigin: true,
|
|
58
|
+
ws: true,
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
} else if (webpack !== false) {
|
|
62
|
+
app.use(express.static("./dist"));
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
app.get("/health", (req, res) => {
|
|
@@ -60,15 +68,19 @@ export default function createAppServer(opts = { port: 3000 }) {
|
|
|
60
68
|
});
|
|
61
69
|
});
|
|
62
70
|
|
|
63
|
-
|
|
71
|
+
if (start) {
|
|
72
|
+
const server = app.listen(port);
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
/* eslint-disable no-console */
|
|
75
|
+
console.log("Starting in %s mode", process.env.NODE_ENV);
|
|
76
|
+
console.log(
|
|
77
|
+
"Listening at http://%s:%s",
|
|
78
|
+
server.address().address === "::"
|
|
79
|
+
? "localhost"
|
|
80
|
+
: server.address().address,
|
|
81
|
+
server.address().port
|
|
82
|
+
);
|
|
83
|
+
}
|
|
72
84
|
|
|
73
85
|
return app;
|
|
74
86
|
}
|
package/src/formatOutput.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { camelCase, isArray, isDate } from "lodash-es";
|
|
1
|
+
import { camelCase, isObject, isArray, isDate, isEmpty, omit } from "lodash-es";
|
|
2
2
|
import convertCase from "./convertCase.js";
|
|
3
3
|
|
|
4
|
-
export default function formatOutput(
|
|
4
|
+
export default function formatOutput(o, omittedFields = []) {
|
|
5
|
+
const obj =
|
|
6
|
+
isObject(o) && !isEmpty(omittedFields) ? omit(o, omittedFields) : o;
|
|
5
7
|
if (isDate(obj)) {
|
|
6
8
|
return obj.toISOString();
|
|
7
9
|
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { default as createAppServer } from "./createAppServer.js";
|
|
1
|
+
export { default as createAppServer, DEFAULTS } from "./createAppServer.js";
|
|
2
2
|
export { default as convertCase } from "./convertCase.js";
|
|
3
3
|
export { default as formatInput } from "./formatInput.js";
|
|
4
4
|
export { default as formatOutput } from "./formatOutput.js";
|
|
5
5
|
export * from "./asyncJsonHandler.js";
|
|
6
|
+
export { default as schemaHandler } from "./schemaHandler.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import asyncJsonHandler from "./asyncJsonHandler.js";
|
|
2
|
+
|
|
3
|
+
export default function schemaHandler(schema, fn) {
|
|
4
|
+
return async (req, res, next) => {
|
|
5
|
+
// Validate the input schema
|
|
6
|
+
try {
|
|
7
|
+
const value = await schema.validateAsync(req.body, {
|
|
8
|
+
// False here will not allow keys that are not part of the schema
|
|
9
|
+
allowUnknown: false,
|
|
10
|
+
// True here will strip the unknown keys from the returned value
|
|
11
|
+
stripUnknown: true,
|
|
12
|
+
// Ensure that all rules are evaluated, by default Joi stops on the first error
|
|
13
|
+
abortEarly: false,
|
|
14
|
+
// Customized error messages
|
|
15
|
+
messages: {
|
|
16
|
+
"any.invalid": "{{#label}} is invalid",
|
|
17
|
+
"any.required": "{{#label}} is required",
|
|
18
|
+
"boolean.base": "{{#label}} must be true or false",
|
|
19
|
+
"string.empty": "{{#label}} is required",
|
|
20
|
+
"string.email": "{{#label}} must be a valid email address",
|
|
21
|
+
"string.min":
|
|
22
|
+
"{{#label}} must be at least {{#limit}} characters long",
|
|
23
|
+
"string.max":
|
|
24
|
+
"{{#label}} cannot be more than {{#limit}} characters long",
|
|
25
|
+
"number.base": "{{#label}} must be a number",
|
|
26
|
+
"date.base": "{{#label}} must be a valid date",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line require-atomic-updates
|
|
31
|
+
req.body = value;
|
|
32
|
+
|
|
33
|
+
// Wrap all of these in our async handler
|
|
34
|
+
await asyncJsonHandler(fn)(req, res, next);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
res.status(400).json({
|
|
37
|
+
errors: Object.assign.apply(
|
|
38
|
+
null,
|
|
39
|
+
error.details.map((d) => ({ [d.path]: d.message }))
|
|
40
|
+
),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|