@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stanlemon/server",
3
- "version": "0.1.2",
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
+ }
@@ -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.message });
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.message });
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.message });
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.message });
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.message });
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
  }
@@ -8,39 +8,36 @@ import { createProxyMiddleware } from "http-proxy-middleware";
8
8
 
9
9
  dotenv.config();
10
10
 
11
- const DEVELOPMENT = "development";
12
- const { NODE_ENV = DEVELOPMENT, WEBPACK_URL = "http://localhost:8080" } =
13
- process.env;
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 !== DEVELOPMENT) {
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: WEBPACK_URL,
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: WEBPACK_URL,
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
- const server = app.listen(opts.port);
71
+ if (start) {
72
+ const server = app.listen(port);
64
73
 
65
- /* eslint-disable no-console */
66
- console.log("Starting in %s mode", NODE_ENV);
67
- console.log(
68
- "Listening at http://%s:%s",
69
- server.address().address === "::" ? "localhost" : server.address().address,
70
- server.address().port
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
  }
@@ -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(obj) {
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
+ }