express-generator-typescript 3.0.2 → 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 +6 -2
- package/lib/template/README.md +1 -1
- package/lib/template/package.json +4 -2
- package/lib/template/src/common/classes/route-errors.ts +9 -0
- package/lib/template/src/common/constants/HttpStatusCodes.ts +2 -7
- package/lib/template/src/common/constants/Paths.ts +5 -11
- package/lib/template/src/common/constants/environment-consts.ts +44 -0
- package/lib/template/src/common/utils/validators.ts +28 -0
- package/lib/template/src/entities/User/types.ts +1 -1
- package/lib/template/src/main.ts +3 -4
- package/lib/template/src/public/scripts/users.js +1 -1
- package/lib/template/src/repos/MockOrm.ts +2 -2
- package/lib/template/src/repos/UserRepo.ts +15 -12
- package/lib/template/src/repos/common/database.json +0 -6
- package/lib/template/src/routes/UserRoutes.ts +12 -12
- package/lib/template/src/routes/apiRouter.ts +5 -5
- package/lib/template/src/server.ts +8 -10
- package/lib/template/src/services/UserService.ts +9 -14
- package/lib/template/tests/common/comparators.ts +5 -7
- package/lib/template/tests/users.test.ts +115 -122
- package/package.json +1 -1
- package/lib/template/src/common/constants/env-inv.ts +0 -32
- package/lib/template/src/common/types/utility-types.ts +0 -5
- /package/lib/template/src/entities/common/{types.ts → entity-base-types.ts} +0 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
package/lib/template/README.md
CHANGED
|
@@ -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
|
|
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.
|
|
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 '../
|
|
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
|
-
//
|
|
4
|
+
// EXPORT //
|
|
5
5
|
// ========================================================================= //
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
export default jetPaths({
|
|
8
|
+
$path: '/api',
|
|
9
9
|
Users: {
|
|
10
|
-
|
|
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
|
+
}
|
package/lib/template/src/main.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import logger from 'jet-logger';
|
|
2
2
|
|
|
3
|
-
import { EnvVars } from './common/constants/
|
|
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.
|
|
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
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
|
38
|
-
const { user } =
|
|
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
|
|
49
|
-
const { user } =
|
|
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
|
|
57
|
+
* @route DELETE /api/users/delete/:id
|
|
58
58
|
*/
|
|
59
|
-
async function
|
|
60
|
-
const { id } =
|
|
61
|
-
await UserService.
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
18
|
-
userRouter.post(Paths.Users.Add
|
|
19
|
-
userRouter.put(Paths.Users.Update
|
|
20
|
-
userRouter.delete(Paths.Users.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
|
|
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/
|
|
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.
|
|
31
|
+
if (EnvVars.NODE_ENV === NodeEnvs.DEVELOPMENT) {
|
|
32
32
|
app.use(morgan('dev'));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Security
|
|
36
|
-
if (EnvVars.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
39
|
-
dbUsers = await
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
vi.spyOn(UserService, 'getAll')
|
|
65
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
`
|
|
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(
|
|
85
|
+
.post(PATH_USERS_ADD)
|
|
84
86
|
.send({ user: input });
|
|
85
87
|
expect(res.status).toBe(CREATED);
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
'
|
|
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(
|
|
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
|
-
|
|
107
|
-
`
|
|
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(
|
|
110
|
+
.post(PATH_USERS_ADD)
|
|
112
111
|
.send({ user: { name: '', email: 'a@a.com' } });
|
|
113
112
|
expect(res.status).toBe(BAD_REQUEST);
|
|
114
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
`
|
|
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(
|
|
124
|
+
const res = await agent.put(PATH_USERS_UPDATE).send({ user });
|
|
127
125
|
expect(res.status).toBe(OK);
|
|
128
|
-
const
|
|
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
|
-
|
|
134
|
-
'
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.
|
|
140
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(
|
|
156
|
-
}
|
|
157
|
-
);
|
|
154
|
+
expect(res.body.error).toBe(ERR_USER_NOT_FOUND);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
158
157
|
});
|
|
159
158
|
|
|
160
159
|
// ---- `Delete`
|
|
161
|
-
describe(`"DELETE
|
|
162
|
-
|
|
163
|
-
`
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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(
|
|
183
|
-
}
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
`
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
196
|
-
`
|
|
197
|
-
|
|
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
|
+
"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
|
-
});
|
|
File without changes
|