express-generator-typescript 3.0.3 → 3.0.4

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/README.md CHANGED
@@ -72,7 +72,9 @@ npm run dev
72
72
 
73
73
  The generated project is a small CRUD app for a `User` record. It shows how to structure models, services, and routes in Express + TypeScript. Linting, formatting, building, and hot reloading are all set up for you.
74
74
 
75
- ### Available `package.json` Scripts
75
+ ---
76
+
77
+ #### Available `package.json` scripts
76
78
 
77
79
  - `npm run dev` – Run the server in development with live reload and browser refresh.
78
80
  - `npm test` – Run the tests with Vitest.
@@ -84,7 +86,9 @@ The generated project is a small CRUD app for a `User` record. It shows how to s
84
86
  - `npm run typecheck` – Check for TypeScript errors without building.
85
87
  - `npm run install:clean` – Delete `node_modules` and the lockfile, then reinstall.
86
88
 
87
- ### Architecture
89
+ ---
90
+
91
+ #### Architecture
88
92
 
89
93
  The app uses a **layered** architecture, which suits a small CRUD app. If you plan to grow it, consider switching to a **domain-based** layout. The [Typescript Best Practices README](https://github.com/seanpmaxwell/Typescript-Best-Practices/tree/main?tab=readme-ov-file#architecture) explains both patterns.
90
94
 
@@ -53,7 +53,7 @@ Delete `node_modules/` and `package-lock.json`, then reinstall all dependencies.
53
53
  - **Request logging**: [morgan](https://github.com/expressjs/morgan) (development only)
54
54
  - **General logging**: [jet-logger](https://github.com/seanpmaxwell/jet-logger)
55
55
  - **Validation**: [jet-validators](https://github.com/seanpmaxwell/jet-validators)
56
- - **Environment variables**: [dotenv](https://github.com/motdotla/dotenv) loads `config/.env.*`, and [jet-env](https://github.com/seanpmaxwell/jet-env) validates them
56
+ - **Environment variables**: [dotenv](https://github.com/motdotla/dotenv) loads `config/.env.*`
57
57
  - **Reloading**: [tsx](https://tsx.hirok.io) (`tsx watch` restarts the server) and [livereload](https://github.com/napcs/node-livereload) + [connect-livereload](https://github.com/intesso/connect-livereload) (refreshes the browser)
58
58
  - **Testing**: [Vitest](https://vitest.dev) + [Supertest](https://github.com/ladjs/supertest)
59
59
  - **Linting**: [ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint) and [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n)
@@ -21,10 +21,9 @@
21
21
  "dotenv": "^18.0.4",
22
22
  "express": "^5.2.1",
23
23
  "helmet": "^8.3.0",
24
- "jet-env": "^1.1.6",
25
24
  "jet-id": "^1.3.1",
26
25
  "jet-logger": "3.0.0",
27
- "jet-paths": "^4.0.2",
26
+ "jet-paths": "^4.1.3",
28
27
  "jet-validators": "^2.3.1",
29
28
  "jsonfile": "^6.2.1",
30
29
  "morgan": "^1.12.1"
@@ -53,5 +52,8 @@
53
52
  "typescript": "^6.0.3",
54
53
  "typescript-eslint": "^8.70.1",
55
54
  "vitest": "^5.0.2"
55
+ },
56
+ "allowScripts": {
57
+ "esbuild@0.28.2": true
56
58
  }
57
59
  }
@@ -25,6 +25,15 @@ export class RouteError extends Error {
25
25
  }
26
26
  }
27
27
 
28
+ /**
29
+ * Don't have to manually pass `HttpStatusCodes.NOT_FOUND` each time.
30
+ */
31
+ export class NotFoundError extends RouteError {
32
+ public constructor(message: string) {
33
+ super(HttpStatusCodes.NOT_FOUND, message);
34
+ }
35
+ }
36
+
28
37
  /**
29
38
  * Request data failed schema validation.
30
39
  */
@@ -1,4 +1,4 @@
1
- import type { ValueOf } from '../types/utility-types';
1
+ import type { ValueOf } from '../utils/validators';
2
2
 
3
3
  // ========================================================================= //
4
4
  // CONSTANTS //
@@ -385,14 +385,9 @@ const HttpStatusCodes = {
385
385
  NETWORK_AUTHENTICATION_REQUIRED: 511,
386
386
  } as const;
387
387
 
388
- // ========================================================================= //
389
- // TYPES //
390
- // ========================================================================= //
391
-
392
- type HttpStatusCodes = ValueOf<typeof HttpStatusCodes>;
393
-
394
388
  // ========================================================================= //
395
389
  // EXPORT //
396
390
  // ========================================================================= //
397
391
 
392
+ type HttpStatusCodes = ValueOf<typeof HttpStatusCodes>;
398
393
  export default HttpStatusCodes;
@@ -1,22 +1,16 @@
1
1
  import jetPaths from 'jet-paths';
2
2
 
3
3
  // ========================================================================= //
4
- // CONSTANTS //
4
+ // EXPORT //
5
5
  // ========================================================================= //
6
6
 
7
- const Paths = jetPaths({
8
- _: '/api',
7
+ export default jetPaths({
8
+ $path: '/api',
9
9
  Users: {
10
- _: '/users',
10
+ $path: '/users',
11
11
  Get: '/all',
12
12
  Add: '/add',
13
13
  Update: '/update',
14
- Delete: '/delete',
14
+ Delete: '/delete/:id',
15
15
  },
16
16
  } as const);
17
-
18
- // ========================================================================= //
19
- // EXPORT //
20
- // ========================================================================= //
21
-
22
- export default Paths;
@@ -0,0 +1,44 @@
1
+ /* eslint-disable no-process-env */
2
+ import logger from 'jet-logger';
3
+
4
+ import { isNumber, isValueOf, type ValueOf } from '../utils/validators';
5
+
6
+ // ========================================================================= //
7
+ // CONSTANTS //
8
+ // ========================================================================= //
9
+
10
+ // NOTE: These need to match the names of your ".env" files
11
+ export const NodeEnvs = {
12
+ DEVELOPMENT: 'development',
13
+ TEST: 'test',
14
+ PRODUCTION: 'production',
15
+ } as const;
16
+ export type NodeEnvs = ValueOf<typeof NodeEnvs>;
17
+
18
+ export const EnvVars = {
19
+ NODE_ENV: process.env.NODE_ENV?.toLowerCase(),
20
+ PORT: Number(process.env.PORT),
21
+ } as const;
22
+
23
+ // ========================================================================= //
24
+ // EXEC //
25
+ // ========================================================================= //
26
+
27
+ // ---- Validation
28
+ // Validate the "environment variables"
29
+ {
30
+ try {
31
+ const isNodeEnv = isValueOf(NodeEnvs);
32
+ if (!isNodeEnv(EnvVars.NODE_ENV)) {
33
+ throw new Error(
34
+ 'process.env.NODE_ENV must be "development", "test", or "production"',
35
+ );
36
+ }
37
+ if (!isNumber(EnvVars.PORT)) {
38
+ throw new Error('process.env.PORT must be a valid number');
39
+ }
40
+ } catch (err) {
41
+ logger.err(err);
42
+ throw err;
43
+ }
44
+ }
@@ -0,0 +1,28 @@
1
+ // ========================================================================= //
2
+ // TYPES //
3
+ // ========================================================================= //
4
+
5
+ export type ValueOf<T> = T[keyof T];
6
+ type Table = Record<string, string> | Record<string, number>;
7
+
8
+ // ========================================================================= //
9
+ // EXEC //
10
+ // ========================================================================= //
11
+
12
+ /**
13
+ * Check if an `unknown` is a number type.
14
+ */
15
+ export function isNumber<T>(val: T): val is Extract<T, number> {
16
+ return typeof val === 'number' && !isNaN(val);
17
+ }
18
+
19
+ /**
20
+ * Check if an `unknown` is a value of an object.
21
+ */
22
+ export function isValueOf<T extends Table>(
23
+ table: T,
24
+ ): (v: unknown) => v is ValueOf<T> {
25
+ const vals = Object.values(table);
26
+ const set = new Set(vals);
27
+ return (val: unknown): val is ValueOf<T> => set.has(val);
28
+ }
@@ -1,4 +1,4 @@
1
- import type { Entity } from '@src/entities/common/types';
1
+ import type { Entity } from '@src/entities/common/entity-base-types';
2
2
 
3
3
  // ========================================================================= //
4
4
  // TYPES //
@@ -1,21 +1,20 @@
1
1
  import logger from 'jet-logger';
2
2
 
3
- import { EnvVars } from './common/constants/env-inv';
3
+ import { EnvVars } from './common/constants/environment-consts';
4
4
  import server from './server';
5
5
 
6
6
  // ========================================================================= //
7
7
  // CONSTANTS //
8
8
  // ========================================================================= //
9
9
 
10
- const SERVER_START_MESSAGE =
11
- 'Express server started on port: ' + EnvVars.Port.toString();
10
+ const SERVER_START_MESSAGE = `Express server started on port: ${EnvVars.PORT}`;
12
11
 
13
12
  // ========================================================================= //
14
13
  // EXEC //
15
14
  // ========================================================================= //
16
15
 
17
16
  // Start the server
18
- server.listen(EnvVars.Port, (err) => {
17
+ server.listen(EnvVars.PORT, (err) => {
19
18
  if (!!err) {
20
19
  logger.err(err.message);
21
20
  } else {
@@ -154,7 +154,7 @@ function submitEdit(ele) {
154
154
  */
155
155
  function deleteUser(ele) {
156
156
  var id = ele.getAttribute('data-user-id');
157
- HttpClient.delete('/api/users/delete?id=' + encodeURIComponent(id))
157
+ HttpClient.delete('/api/users/delete/' + encodeURIComponent(id))
158
158
  .then(checkResponse)
159
159
  .then(() => displayUsers())
160
160
  .catch(showError);
@@ -2,7 +2,7 @@ import fs from 'fs/promises';
2
2
  import jsonfile from 'jsonfile';
3
3
  import path from 'path';
4
4
 
5
- import { EnvVars, NodeEnvs } from '@src/common/constants/env-inv';
5
+ import { EnvVars, NodeEnvs } from '@src/common/constants/environment-consts';
6
6
  import type { UserEntity } from '@src/entities/User';
7
7
 
8
8
  // ========================================================================= //
@@ -12,7 +12,7 @@ import type { UserEntity } from '@src/entities/User';
12
12
  const DATABASE_FILE_PATH = path.join(
13
13
  import.meta.dirname,
14
14
  'common',
15
- EnvVars.NodeEnv === NodeEnvs.TEST ? 'database.test.json' : 'database.json',
15
+ EnvVars.NODE_ENV === NodeEnvs.TEST ? 'database.test.json' : 'database.json',
16
16
  );
17
17
 
18
18
  // ========================================================================= //
@@ -33,7 +33,7 @@ async function getAll(): Promise<UserEntity[]> {
33
33
  /**
34
34
  * Add one user.
35
35
  */
36
- async function add(user: UserEntity): Promise<void> {
36
+ async function addOne(user: UserEntity): Promise<void> {
37
37
  const db = await orm.openDb();
38
38
  db.users.push(user);
39
39
  return orm.saveDb(db);
@@ -42,7 +42,7 @@ async function add(user: UserEntity): Promise<void> {
42
42
  /**
43
43
  * Update a user.
44
44
  */
45
- async function update(user: UserEntity): Promise<void> {
45
+ async function updateOne(user: UserEntity): Promise<void> {
46
46
  const db = await orm.openDb();
47
47
  for (let i = 0; i < db.users.length; i++) {
48
48
  if (db.users[i].id === user.id) {
@@ -60,7 +60,7 @@ async function update(user: UserEntity): Promise<void> {
60
60
  /**
61
61
  * Delete one user.
62
62
  */
63
- async function delete_(id: string): Promise<void> {
63
+ async function deleteOne(id: string): Promise<void> {
64
64
  const db = await orm.openDb();
65
65
  for (let i = 0; i < db.users.length; i++) {
66
66
  if (db.users[i].id === id) {
@@ -73,9 +73,9 @@ async function delete_(id: string): Promise<void> {
73
73
  // ============================ Unit-tests Only ============================ //
74
74
 
75
75
  /**
76
- * @testOnly
77
- *
78
76
  * Delete every user record.
77
+ *
78
+ * @testOnly
79
79
  */
80
80
  async function deleteAllUsers(): Promise<void> {
81
81
  const db = await orm.openDb();
@@ -84,9 +84,9 @@ async function deleteAllUsers(): Promise<void> {
84
84
  }
85
85
 
86
86
  /**
87
- * @testOnly
88
- *
89
87
  * Insert copies of `users` with fresh ids; the inputs are not modified.
88
+ *
89
+ * @testOnly
90
90
  */
91
91
  async function insertMultiple(
92
92
  users: UserEntity[] | readonly UserEntity[],
@@ -106,12 +106,15 @@ async function insertMultiple(
106
106
  // EXPORT //
107
107
  // ========================================================================= //
108
108
 
109
+ export const UserRepoTestOnly = {
110
+ deleteAllUsers,
111
+ insertMultiple,
112
+ } as const;
113
+
109
114
  export default {
110
115
  persists,
111
116
  getAll,
112
- add,
113
- update,
114
- delete: delete_,
115
- deleteAllUsers,
116
- insertMultiple,
117
+ addOne,
118
+ updateOne,
119
+ deleteOne,
117
120
  } as const;
@@ -9,7 +9,7 @@ import parseReq from './common/parseReq';
9
9
  // EXEC //
10
10
  // ========================================================================= //
11
11
 
12
- const reqValidators = {
12
+ const validators = {
13
13
  add: parseReq({ user: User.isInput }),
14
14
  update: parseReq({ user: User.isComplete }),
15
15
  delete: parseReq({ id: User.isId }),
@@ -34,8 +34,8 @@ async function getAll(_: Req, res: Res) {
34
34
  *
35
35
  * @route POST /api/users/add
36
36
  */
37
- async function add(req: Req, res: Res) {
38
- const { user } = reqValidators.add(req.body);
37
+ async function addOne(req: Req, res: Res) {
38
+ const { user } = validators.add(req.body);
39
39
  const created = await UserService.addOne(user);
40
40
  res.status(HttpStatusCodes.CREATED).json({ user: created });
41
41
  }
@@ -45,8 +45,8 @@ async function add(req: Req, res: Res) {
45
45
  *
46
46
  * @route PUT /api/users/update
47
47
  */
48
- async function update(req: Req, res: Res) {
49
- const { user } = reqValidators.update(req.body);
48
+ async function updateOne(req: Req, res: Res) {
49
+ const { user } = validators.update(req.body);
50
50
  await UserService.updateOne(user);
51
51
  res.status(HttpStatusCodes.OK).end();
52
52
  }
@@ -54,11 +54,11 @@ async function update(req: Req, res: Res) {
54
54
  /**
55
55
  * Delete one user.
56
56
  *
57
- * @route DELETE /api/users/delete?id=:id
57
+ * @route DELETE /api/users/delete/:id
58
58
  */
59
- async function delete_(req: Req, res: Res) {
60
- const { id } = reqValidators.delete(req.query);
61
- await UserService.delete(id);
59
+ async function deleteOne(req: Req, res: Res) {
60
+ const { id } = validators.delete(req.params);
61
+ await UserService.deleteOne(id);
62
62
  res.status(HttpStatusCodes.OK).end();
63
63
  }
64
64
 
@@ -68,7 +68,7 @@ async function delete_(req: Req, res: Res) {
68
68
 
69
69
  export default {
70
70
  getAll,
71
- add,
72
- update,
73
- delete: delete_,
71
+ addOne,
72
+ updateOne,
73
+ deleteOne,
74
74
  } as const;
@@ -14,12 +14,12 @@ const apiRouter = Router();
14
14
 
15
15
  const userRouter = Router();
16
16
 
17
- userRouter.get(Paths.Users.Get._, UserRoutes.getAll);
18
- userRouter.post(Paths.Users.Add._, UserRoutes.add);
19
- userRouter.put(Paths.Users.Update._, UserRoutes.update);
20
- userRouter.delete(Paths.Users.Delete._, UserRoutes.delete);
17
+ userRouter.get(Paths.Users.Get.$path, UserRoutes.getAll);
18
+ userRouter.post(Paths.Users.Add.$path, UserRoutes.addOne);
19
+ userRouter.put(Paths.Users.Update.$path, UserRoutes.updateOne);
20
+ userRouter.delete(Paths.Users.Delete.$path, UserRoutes.deleteOne);
21
21
 
22
- apiRouter.use(Paths.Users._, userRouter);
22
+ apiRouter.use(Paths.Users.$path, userRouter);
23
23
 
24
24
  // ========================================================================= //
25
25
  // EXPORT //
@@ -8,12 +8,12 @@ import logger from 'jet-logger';
8
8
  import morgan from 'morgan';
9
9
  import path from 'path';
10
10
 
11
+ import { RouteError } from '@src/common/classes/route-errors';
11
12
  import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
12
13
  import Paths from '@src/common/constants/Paths';
13
- import { RouteError } from '@src/common/classes/route-errors';
14
14
  import BaseRouter from '@src/routes/apiRouter';
15
15
 
16
- import { EnvVars, NodeEnvs } from './common/constants/env-inv';
16
+ import { EnvVars, NodeEnvs } from './common/constants/environment-consts';
17
17
 
18
18
  // ========================================================================= //
19
19
  // EXEC //
@@ -28,17 +28,17 @@ app.use(express.json());
28
28
  app.use(express.urlencoded({ extended: true }));
29
29
 
30
30
  // Show routes called in console during development
31
- if (EnvVars.NodeEnv === NodeEnvs.DEV) {
31
+ if (EnvVars.NODE_ENV === NodeEnvs.DEVELOPMENT) {
32
32
  app.use(morgan('dev'));
33
33
  }
34
34
 
35
35
  // Security
36
- if (EnvVars.NodeEnv === NodeEnvs.PRODUCTION) {
36
+ if (EnvVars.NODE_ENV === NodeEnvs.PRODUCTION) {
37
37
  app.use(helmet());
38
38
  }
39
39
 
40
40
  // Add APIs, must be after middleware
41
- app.use(Paths._, BaseRouter);
41
+ app.use(Paths.$path, BaseRouter);
42
42
 
43
43
  // Add error handler. `_next` must stay: Express only treats 4-arg middleware
44
44
  // as an error handler.
@@ -48,7 +48,7 @@ app.use((err: Error, _: Request, res: Response, _next: NextFunction) => {
48
48
  .status(err.status)
49
49
  .json({ error: err.message, errors: err.errors });
50
50
  }
51
- if (EnvVars.NodeEnv !== NodeEnvs.TEST) {
51
+ if (EnvVars.NODE_ENV !== NodeEnvs.TEST) {
52
52
  logger.err(err, true);
53
53
  }
54
54
  return res
@@ -58,15 +58,13 @@ app.use((err: Error, _: Request, res: Response, _next: NextFunction) => {
58
58
 
59
59
  // =========================== Front-end Content =========================== //
60
60
 
61
- // Views directory (html)
61
+ // Views/HTML directory (html)
62
62
  const viewsDir = path.join(import.meta.dirname, 'views');
63
-
64
- // Set static directory (js and css).
65
63
  const staticDir = path.join(import.meta.dirname, 'public');
66
64
 
67
65
  // Refresh the browser on changes. Imported lazily because livereload is a
68
66
  // dev dependency and isn't installed in production.
69
- if (EnvVars.NodeEnv === NodeEnvs.DEV) {
67
+ if (EnvVars.NODE_ENV === NodeEnvs.DEVELOPMENT) {
70
68
  const { setupLiveReload } = await import('@src/common/utils/dev-only');
71
69
  setupLiveReload(app, [staticDir, viewsDir]);
72
70
  }
@@ -1,5 +1,4 @@
1
- import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
2
- import { RouteError } from '@src/common/classes/route-errors';
1
+ import { NotFoundError } from '@src/common/classes/route-errors';
3
2
  import User, { type UserEntity, type UserInput } from '@src/entities/User';
4
3
  import UserRepo from '@src/repos/UserRepo';
5
4
 
@@ -7,7 +6,7 @@ import UserRepo from '@src/repos/UserRepo';
7
6
  // CONSTANTS //
8
7
  // ========================================================================= //
9
8
 
10
- const Errors = {
9
+ const UserServiceErrors = {
11
10
  USER_NOT_FOUND: 'User not found',
12
11
  } as const;
13
12
 
@@ -27,7 +26,7 @@ function getAll(): Promise<UserEntity[]> {
27
26
  */
28
27
  async function addOne(input: UserInput): Promise<UserEntity> {
29
28
  const user = User.create(input);
30
- await UserRepo.add(user);
29
+ await UserRepo.addOne(user);
31
30
  return user;
32
31
  }
33
32
 
@@ -36,10 +35,8 @@ async function addOne(input: UserInput): Promise<UserEntity> {
36
35
  */
37
36
  async function updateOne(user: UserEntity): Promise<void> {
38
37
  const persists = await UserRepo.persists(user.id);
39
- if (!persists) {
40
- throw new RouteError(HttpStatusCodes.NOT_FOUND, Errors.USER_NOT_FOUND);
41
- }
42
- return UserRepo.update(user);
38
+ if (!persists) throw new NotFoundError(UserServiceErrors.USER_NOT_FOUND);
39
+ return UserRepo.updateOne(user);
43
40
  }
44
41
 
45
42
  /**
@@ -47,10 +44,8 @@ async function updateOne(user: UserEntity): Promise<void> {
47
44
  */
48
45
  async function deleteOne(id: string): Promise<void> {
49
46
  const persists = await UserRepo.persists(id);
50
- if (!persists) {
51
- throw new RouteError(HttpStatusCodes.NOT_FOUND, Errors.USER_NOT_FOUND);
52
- }
53
- return UserRepo.delete(id);
47
+ if (!persists) throw new NotFoundError(UserServiceErrors.USER_NOT_FOUND);
48
+ return UserRepo.deleteOne(id);
54
49
  }
55
50
 
56
51
  // ========================================================================= //
@@ -58,9 +53,9 @@ async function deleteOne(id: string): Promise<void> {
58
53
  // ========================================================================= //
59
54
 
60
55
  export default {
61
- Errors,
56
+ Errors: UserServiceErrors,
62
57
  getAll,
63
58
  addOne,
64
59
  updateOne,
65
- delete: deleteOne,
60
+ deleteOne,
66
61
  } as const;
@@ -16,14 +16,12 @@ type UserArray = UserEntity[] | readonly UserEntity[];
16
16
  */
17
17
  export function compareUserArrays(a: UserArray, b: UserArray): boolean {
18
18
  if (a.length !== b.length) return false;
19
- const aSorted = sortByEmail(a),
20
- bSorted = sortByEmail(b);
19
+ const aSorted = sortByEmail(a);
20
+ const bSorted = sortByEmail(b);
21
21
  for (let i = 0; i < aSorted.length; i++) {
22
- const a = aSorted[i],
23
- b = bSorted[i];
24
- if (a.email !== b.email || a.name !== b.name) {
25
- return false;
26
- }
22
+ const a = aSorted[i];
23
+ const b = bSorted[i];
24
+ if (a.email !== b.email || a.name !== b.name) return false;
27
25
  }
28
26
  return true;
29
27
  }
@@ -1,10 +1,10 @@
1
1
  import jetid from 'jet-id';
2
2
 
3
+ import { ValidationError } from '@src/common/classes/route-errors';
3
4
  import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
4
5
  import Paths from '@src/common/constants/Paths';
5
- import { ValidationError } from '@src/common/classes/route-errors';
6
6
  import User, { type UserEntity } from '@src/entities/User';
7
- import UserRepo from '@src/repos/UserRepo';
7
+ import UserRepo, { UserRepoTestOnly } from '@src/repos/UserRepo';
8
8
  import UserService from '@src/services/UserService';
9
9
 
10
10
  import { compareUserArrays } from './common/comparators';
@@ -15,15 +15,23 @@ import { agent } from './support/agent';
15
15
  // CONSTANTS //
16
16
  // ========================================================================= //
17
17
 
18
+ // Path constants
19
+ const PATH_USERS_GET = Paths.Users.Get();
20
+ const PATH_USERS_ADD = Paths.Users.Add();
21
+ const PATH_USERS_UPDATE = Paths.Users.Update();
22
+ const PATH_USERS_DELETE = Paths.Users.Delete.$tmpl;
23
+
24
+ // Errors
25
+ const ERR_USER_NOT_FOUND = UserService.Errors.USER_NOT_FOUND;
26
+ const { BAD_REQUEST, CREATED, INTERNAL_SERVER_ERROR, OK, NOT_FOUND } =
27
+ HttpStatusCodes;
28
+
18
29
  const DUMMY_USERS = [
19
30
  User.of('Sean Maxwell', 'sean.maxwell@gmail.com'),
20
31
  User.of('John Smith', 'john.smith@gmail.com'),
21
32
  User.of('Gordan Freeman', 'gordan.freeman@gmail.com'),
22
33
  ] as const;
23
34
 
24
- const { BAD_REQUEST, CREATED, INTERNAL_SERVER_ERROR, OK, NOT_FOUND } =
25
- HttpStatusCodes;
26
-
27
35
  // ========================================================================= //
28
36
  // TESTS //
29
37
  // ========================================================================= //
@@ -35,8 +43,8 @@ describe('UserRouter', () => {
35
43
  let dbUsers: UserEntity[] = [];
36
44
 
37
45
  beforeEach(async () => {
38
- await UserRepo.deleteAllUsers();
39
- dbUsers = await UserRepo.insertMultiple(DUMMY_USERS);
46
+ await UserRepoTestOnly.deleteAllUsers();
47
+ dbUsers = await UserRepoTestOnly.insertMultiple(DUMMY_USERS);
40
48
  });
41
49
 
42
50
  afterEach(() => {
@@ -44,162 +52,147 @@ describe('UserRouter', () => {
44
52
  });
45
53
 
46
54
  // ---- `Get`
47
- describe(`"GET:${Paths.Users.Get()}"`, () => {
48
- it(
49
- 'should return a JSON object with all the users and a status code of ' +
50
- `"${OK}" if the request was successful.`,
51
- async () => {
52
- const res: TestRes<{ users: UserEntity[] }> = await agent.get(
53
- Paths.Users.Get(),
54
- );
55
+ describe(`"GET - ${PATH_USERS_GET}"`, () => {
56
+ describe('on success', () => {
57
+ it(`returns all users as JSON with status "${OK}"`, async () => {
58
+ const res: TestRes<{ users: UserEntity[] }> =
59
+ await agent.get(PATH_USERS_GET);
55
60
  expect(res.status).toBe(OK);
56
- expect(compareUserArrays(res.body.users, DUMMY_USERS)).toBeTruthy();
57
- },
58
- );
59
-
60
- it(
61
- `should return a JSON error and a status code of ` +
62
- `"${INTERNAL_SERVER_ERROR}" if an unexpected error is thrown.`,
63
- async () => {
64
- vi.spyOn(UserService, 'getAll').mockRejectedValueOnce(
65
- new Error('boom'),
66
- );
67
- const res: TestRes = await agent.get(Paths.Users.Get());
61
+ const usersMatch = compareUserArrays(res.body.users, DUMMY_USERS);
62
+ expect(usersMatch).toBeTruthy();
63
+ });
64
+ });
65
+
66
+ describe('when an unexpected error is thrown', () => {
67
+ it(`returns status "${INTERNAL_SERVER_ERROR}"`, async () => {
68
+ const error = new Error('boom');
69
+ const getAllSpy = vi.spyOn(UserService, 'getAll');
70
+ getAllSpy.mockRejectedValueOnce(error);
71
+ const res: TestRes = await agent.get(PATH_USERS_GET);
68
72
  expect(res.status).toBe(INTERNAL_SERVER_ERROR);
69
73
  expect(res.body.error).toBe('Internal Server Error');
70
- },
71
- );
74
+ });
75
+ });
72
76
  });
73
77
 
74
78
  // ---- `Add`
75
- describe(`"POST:${Paths.Users.Add()}"`, () => {
76
- it(
77
- `should return a status code of "${CREATED}" and persist the user ` +
78
- 'with a server-generated id if the request was successful.',
79
- async () => {
79
+ describe(`"POST - ${PATH_USERS_ADD}"`, () => {
80
+ describe('on success', () => {
81
+ it(`persists the user and returns status "${CREATED}"`, async () => {
80
82
  // Same payload shape the front-end sends
81
83
  const input = { name: 'a', email: 'a@a.com' };
82
84
  const res: TestRes<{ user: UserEntity }> = await agent
83
- .post(Paths.Users.Add())
85
+ .post(PATH_USERS_ADD)
84
86
  .send({ user: input });
85
87
  expect(res.status).toBe(CREATED);
86
- expect(User.isId(res.body.user.id)).toBe(true);
88
+ const isId = User.isId(res.body.user.id);
89
+ expect(isId).toBe(true);
87
90
  const all = await UserRepo.getAll();
88
91
  expect(all).toHaveLength(DUMMY_USERS.length + 1);
89
92
  expect(all).toContainEqual(res.body.user);
90
- },
91
- );
93
+ });
94
+ });
92
95
 
93
- it(
94
- 'should return a JSON object with an error message and a status ' +
95
- `code of "${BAD_REQUEST}" if the user param was missing.`,
96
- async () => {
96
+ describe('when the user param is missing', () => {
97
+ it('returns a validation error', async () => {
97
98
  const res: TestRes = await agent
98
- .post(Paths.Users.Add())
99
+ .post(PATH_USERS_ADD)
99
100
  .send({ user: null });
100
101
  expect(res.status).toBe(BAD_REQUEST);
101
102
  expect(res.body.error).toBe(ValidationError.MESSAGE);
102
103
  expect(res.body.errors?.[0].key).toStrictEqual('user');
103
- },
104
- );
104
+ });
105
+ });
105
106
 
106
- it(
107
- `should return a status code of "${BAD_REQUEST}" if the name is ` +
108
- 'empty.',
109
- async () => {
107
+ describe('when the name is empty', () => {
108
+ it(`returns status "${BAD_REQUEST}"`, async () => {
110
109
  const res: TestRes = await agent
111
- .post(Paths.Users.Add())
110
+ .post(PATH_USERS_ADD)
112
111
  .send({ user: { name: '', email: 'a@a.com' } });
113
112
  expect(res.status).toBe(BAD_REQUEST);
114
- expect(await UserRepo.getAll()).toHaveLength(DUMMY_USERS.length);
115
- },
116
- );
113
+ const allUsers = await UserRepo.getAll();
114
+ expect(allUsers).toHaveLength(DUMMY_USERS.length);
115
+ });
116
+ });
117
117
  });
118
118
 
119
119
  // ---- `Update`
120
- describe(`"PUT:${Paths.Users.Update()}"`, () => {
121
- it(
122
- `should return a status code of "${OK}" and save the change if the ` +
123
- 'request was successful.',
124
- async () => {
120
+ describe(`"PUT - ${PATH_USERS_UPDATE}"`, () => {
121
+ describe('on success', () => {
122
+ it(`saves the change and returns status "${OK}"`, async () => {
125
123
  const user = { ...dbUsers[0], name: 'Bill' };
126
- const res = await agent.put(Paths.Users.Update()).send({ user });
124
+ const res = await agent.put(PATH_USERS_UPDATE).send({ user });
127
125
  expect(res.status).toBe(OK);
128
- const saved = (await UserRepo.getAll()).find((u) => u.id === user.id);
126
+ const allUsers = await UserRepo.getAll();
127
+ const saved = allUsers.find((u) => u.id === user.id);
129
128
  expect(saved?.name).toBe('Bill');
130
- },
131
- );
132
-
133
- it(
134
- 'should return a JSON object with an error message and a status code ' +
135
- `of "${BAD_REQUEST}" if id is not a valid id`,
136
- async () => {
137
- const user = { ...User.create(), name: 'a', email: 'a@a.com', id: '5' };
138
- const res: TestRes = await agent
139
- .put(Paths.Users.Update())
140
- .send({ user });
129
+ });
130
+ });
131
+
132
+ describe('when the id is invalid', () => {
133
+ it('returns a validation error', async () => {
134
+ const newUser = User.create();
135
+ const user = {
136
+ ...newUser,
137
+ name: 'a',
138
+ email: 'a@a.com',
139
+ id: '5',
140
+ };
141
+ const res: TestRes = await agent.put(PATH_USERS_UPDATE).send({ user });
141
142
  expect(res.status).toBe(BAD_REQUEST);
142
143
  expect(res.body.error).toBe(ValidationError.MESSAGE);
143
144
  expect(res.body.errors?.[0].keyPath).toStrictEqual(['user', 'id']);
144
- },
145
- );
146
-
147
- it(
148
- 'should return a JSON object with the error message of ' +
149
- `"${UserService.Errors.USER_NOT_FOUND}" and a status code of ` +
150
- `"${NOT_FOUND}" if the id was not found.`,
151
- async () => {
152
- const user = User.create({ id: jetid(), name: 'a', email: 'a@a.com' }),
153
- res: TestRes = await agent.put(Paths.Users.Update()).send({ user });
145
+ });
146
+ });
147
+
148
+ describe('when the user is not found', () => {
149
+ it(`returns status "${NOT_FOUND}"`, async () => {
150
+ const id = jetid();
151
+ const user = User.create({ id, name: 'a', email: 'a@a.com' });
152
+ const res: TestRes = await agent.put(PATH_USERS_UPDATE).send({ user });
154
153
  expect(res.status).toBe(NOT_FOUND);
155
- expect(res.body.error).toBe(UserService.Errors.USER_NOT_FOUND);
156
- },
157
- );
154
+ expect(res.body.error).toBe(ERR_USER_NOT_FOUND);
155
+ });
156
+ });
158
157
  });
159
158
 
160
159
  // ---- `Delete`
161
- describe(`"DELETE:${Paths.Users.Delete()}"`, () => {
162
- it(
163
- `should return a status code of "${OK}" and remove the user if the ` +
164
- 'request was successful.',
165
- async () => {
166
- const id = dbUsers[0].id,
167
- res = await agent.delete(Paths.Users.Delete({ id }));
160
+ describe(`"DELETE - ${PATH_USERS_DELETE}"`, () => {
161
+ describe('on success', () => {
162
+ it(`removes the user and returns status "${OK}"`, async () => {
163
+ const id = dbUsers[0].id;
164
+ const path = Paths.Users.Delete({ id });
165
+ const res = await agent.delete(path);
168
166
  expect(res.status).toBe(OK);
169
- expect(await UserRepo.persists(id)).toBe(false);
170
- },
171
- );
172
-
173
- it(
174
- 'should return a JSON object with the error message of ' +
175
- `"${UserService.Errors.USER_NOT_FOUND}" and a status code of ` +
176
- `"${NOT_FOUND}" if the id was not found.`,
177
- async () => {
178
- const res: TestRes = await agent.delete(
179
- Paths.Users.Delete({ id: jetid() }),
180
- );
167
+ const persists = await UserRepo.persists(id);
168
+ expect(persists).toBe(false);
169
+ });
170
+ });
171
+
172
+ describe('when the user is not found', () => {
173
+ it(`returns status "${NOT_FOUND}"`, async () => {
174
+ const id = jetid();
175
+ const path = Paths.Users.Delete({ id });
176
+ const res: TestRes = await agent.delete(path);
181
177
  expect(res.status).toBe(NOT_FOUND);
182
- expect(res.body.error).toBe(UserService.Errors.USER_NOT_FOUND);
183
- },
184
- );
185
-
186
- it(
187
- `should return a status code of "${BAD_REQUEST}" if the id is not a ` +
188
- 'valid id.',
189
- async () => {
190
- const res = await agent.delete(Paths.Users.Delete({ id: -1 }));
178
+ expect(res.body.error).toBe(ERR_USER_NOT_FOUND);
179
+ });
180
+ });
181
+
182
+ describe('when the id is invalid', () => {
183
+ it(`returns status "${BAD_REQUEST}"`, async () => {
184
+ const path = Paths.Users.Delete({ id: -1 });
185
+ const res = await agent.delete(path);
191
186
  expect(res.status).toBe(BAD_REQUEST);
192
- },
193
- );
194
-
195
- it(
196
- `should return a status code of "${BAD_REQUEST}" if the id query ` +
197
- 'param is missing.',
198
- async () => {
199
- const res: TestRes = await agent.delete(Paths.Users.Delete());
187
+ });
188
+ });
189
+
190
+ describe('when the id path param is missing', () => {
191
+ it(`returns status "${BAD_REQUEST}"`, async () => {
192
+ const res: TestRes = await agent.delete(PATH_USERS_DELETE);
200
193
  expect(res.status).toBe(BAD_REQUEST);
201
194
  expect(res.body.errors?.[0].key).toStrictEqual('id');
202
- },
203
- );
195
+ });
196
+ });
204
197
  });
205
198
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-generator-typescript",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "Generate new Express applications similar to express-generate which but sets it up to use TypeScript instead",
5
5
  "scripts": {
6
6
  "commit": "git add -A && git commit -m \"dev commit\"",
@@ -1,32 +0,0 @@
1
- import jetEnv, { num } from 'jet-env';
2
-
3
- import type { ValueOf } from '../types/utility-types';
4
-
5
- // ========================================================================= //
6
- // CONSTANTS //
7
- // ========================================================================= //
8
-
9
- // NOTE: These need to match the names of your ".env" files
10
- export const NodeEnvs = {
11
- DEV: 'development',
12
- TEST: 'test',
13
- PRODUCTION: 'production',
14
- } as const;
15
- export type NodeEnvs = ValueOf<typeof NodeEnvs>;
16
-
17
- // ========================================================================= //
18
- // EXEC //
19
- // ========================================================================= //
20
-
21
- // Setup the is `NodeEnvs` validator
22
- const isNodeEnv = (() => {
23
- const vals = Object.values(NodeEnvs);
24
- const valsFin = vals.map((item) => item.toLowerCase());
25
- const set = new Set(valsFin);
26
- return (val: unknown): val is NodeEnvs => set.has(val as NodeEnvs);
27
- })();
28
-
29
- export const EnvVars = jetEnv({
30
- NodeEnv: isNodeEnv,
31
- Port: num,
32
- });
@@ -1,5 +0,0 @@
1
- // ========================================================================= //
2
- // TYPES //
3
- // ========================================================================= //
4
-
5
- export type ValueOf<T> = T[keyof T];