express-generator-typescript 3.0.4 → 3.1.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/README.md +1 -1
- package/bin/cli.js +1 -20
- package/lib/template/README.md +3 -2
- package/lib/template/package.json +5 -5
- package/lib/template/src/common/classes/route-errors.ts +0 -13
- package/lib/template/src/common/constants/Paths/Paths.ts +29 -0
- package/lib/template/src/common/constants/Paths/helpers.ts +29 -0
- package/lib/template/src/common/constants/Paths/index.ts +1 -0
- package/lib/template/src/common/constants/{environment-consts.ts → env.constants.ts} +13 -15
- package/lib/template/src/common/utils/{date-utils.ts → date.utils.ts} +10 -12
- package/lib/template/src/common/utils/validators.ts +0 -23
- package/lib/template/src/entities/User/module.ts +77 -28
- package/lib/template/src/entities/common/entity-base-types.ts +1 -1
- package/lib/template/src/main.ts +3 -3
- package/lib/template/src/public/scripts/lib/bootstrap.bundle.min.js +0 -1
- package/lib/template/src/public/scripts/users.js +19 -10
- package/lib/template/src/public/stylesheets/lib/bootstrap.min.css +0 -1
- package/lib/template/src/public/stylesheets/users.css +24 -1
- package/lib/template/src/repos/MockOrm.ts +2 -2
- package/lib/template/src/repos/UserRepo.ts +5 -15
- package/lib/template/src/repos/common/database.json +3 -3
- package/lib/template/src/{routes/apiRouter.ts → routers/api.router.ts} +3 -11
- package/lib/template/src/{routes/UserRoutes.ts → routers/user.router.ts} +37 -46
- package/lib/template/src/server.ts +20 -7
- package/lib/template/src/views/users.html +8 -0
- package/lib/template/tests/support/utils.ts +23 -0
- package/lib/template/tests/users.test.ts +99 -50
- package/package.json +6 -5
- package/lib/template/src/common/constants/Paths.ts +0 -16
- package/lib/template/src/entities/User/_internal/validators.ts +0 -47
- package/lib/template/src/routes/common/express-types.ts +0 -9
- package/lib/template/src/routes/common/parseReq.ts +0 -23
- package/lib/template/tests/common/comparators.ts +0 -36
- package/lib/template/tests/common/supertest-types.ts +0 -11
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"users": [
|
|
3
3
|
{
|
|
4
|
-
"id": "
|
|
4
|
+
"id": "01a12230-a18b-721e-bf21-0ee836fa989d",
|
|
5
5
|
"name": "Sean Maxwell",
|
|
6
6
|
"email": "smaxwell@example.com",
|
|
7
7
|
"created": "2024-03-22T05:14:36.252Z"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
|
-
"id": "
|
|
10
|
+
"id": "01a12230-a18c-7473-b1f0-845016f412d3",
|
|
11
11
|
"name": "John Smith",
|
|
12
12
|
"email": "john.smith@example.com",
|
|
13
13
|
"created": "2024-03-22T05:20:55.079Z"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
"id": "
|
|
16
|
+
"id": "01a12230-a18c-7473-b1f1-88ed3ea1d5ae",
|
|
17
17
|
"name": "Gordan Freeman",
|
|
18
18
|
"email": "nova@prospect.com",
|
|
19
19
|
"created": "2024-03-22T05:42:18.895Z"
|
|
@@ -2,7 +2,7 @@ import { Router } from 'express';
|
|
|
2
2
|
|
|
3
3
|
import Paths from '@src/common/constants/Paths';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import userRouter from './user.router';
|
|
6
6
|
|
|
7
7
|
// ========================================================================= //
|
|
8
8
|
// EXEC //
|
|
@@ -10,16 +10,8 @@ import UserRoutes from './UserRoutes';
|
|
|
10
10
|
|
|
11
11
|
const apiRouter = Router();
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
const userRouter = Router();
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
apiRouter.use(Paths.Users.$path, userRouter);
|
|
13
|
+
// Add more routers as your app grows (e.g. `postsRouter`)
|
|
14
|
+
apiRouter.use(Paths.Users.PATH, userRouter);
|
|
23
15
|
|
|
24
16
|
// ========================================================================= //
|
|
25
17
|
// EXPORT //
|
|
@@ -1,74 +1,65 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import validate from 'express-zod-safe';
|
|
3
|
+
|
|
1
4
|
import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
|
|
5
|
+
import Paths from '@src/common/constants/Paths';
|
|
2
6
|
import User from '@src/entities/User';
|
|
3
7
|
import UserService from '@src/services/UserService';
|
|
4
8
|
|
|
5
|
-
import type { Req, Res } from './common/express-types';
|
|
6
|
-
import parseReq from './common/parseReq';
|
|
7
|
-
|
|
8
9
|
// ========================================================================= //
|
|
9
|
-
//
|
|
10
|
+
// CONSTANTS //
|
|
10
11
|
// ========================================================================= //
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// Shorthand for the path strings
|
|
14
|
+
const P = {
|
|
15
|
+
GET: Paths.Users.Get.PATH,
|
|
16
|
+
ADD: Paths.Users.Add.PATH,
|
|
17
|
+
UPDATE: Paths.Users.Update.PATH,
|
|
18
|
+
DELETE: Paths.Users.Delete.PATH,
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
// Middlware `validators`
|
|
22
|
+
const V = {
|
|
23
|
+
add: validate({ body: { user: User.Schemas.Input } }),
|
|
24
|
+
update: validate({ body: { user: User.Schemas.Main } }),
|
|
25
|
+
delete: validate({ params: { id: User.Schemas.Id } }),
|
|
16
26
|
} as const;
|
|
17
27
|
|
|
18
28
|
// ========================================================================= //
|
|
19
|
-
//
|
|
29
|
+
// EXEC //
|
|
20
30
|
// ========================================================================= //
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*/
|
|
27
|
-
async function getAll(_: Req, res: Res) {
|
|
32
|
+
const router = Router();
|
|
33
|
+
|
|
34
|
+
// Get all users
|
|
35
|
+
router.get(P.GET, async (_, res) => {
|
|
28
36
|
const users = await UserService.getAll();
|
|
29
37
|
res.status(HttpStatusCodes.OK).json({ users });
|
|
30
|
-
}
|
|
38
|
+
});
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* @route POST /api/users/add
|
|
36
|
-
*/
|
|
37
|
-
async function addOne(req: Req, res: Res) {
|
|
38
|
-
const { user } = validators.add(req.body);
|
|
40
|
+
// Add one user
|
|
41
|
+
router.post(P.ADD, V.add, async (req, res) => {
|
|
42
|
+
const { user } = req.body;
|
|
39
43
|
const created = await UserService.addOne(user);
|
|
40
44
|
res.status(HttpStatusCodes.CREATED).json({ user: created });
|
|
41
|
-
}
|
|
45
|
+
});
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* @route PUT /api/users/update
|
|
47
|
-
*/
|
|
48
|
-
async function updateOne(req: Req, res: Res) {
|
|
49
|
-
const { user } = validators.update(req.body);
|
|
47
|
+
// Update one user
|
|
48
|
+
router.put(P.UPDATE, V.update, async (req, res) => {
|
|
49
|
+
const { user } = req.body;
|
|
50
50
|
await UserService.updateOne(user);
|
|
51
51
|
res.status(HttpStatusCodes.OK).end();
|
|
52
|
-
}
|
|
52
|
+
});
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* @route DELETE /api/users/delete/:id
|
|
58
|
-
*/
|
|
59
|
-
async function deleteOne(req: Req, res: Res) {
|
|
60
|
-
const { id } = validators.delete(req.params);
|
|
54
|
+
// Delete one user
|
|
55
|
+
router.delete(P.DELETE, V.delete, async (req, res) => {
|
|
56
|
+
const { id } = req.params;
|
|
61
57
|
await UserService.deleteOne(id);
|
|
62
58
|
res.status(HttpStatusCodes.OK).end();
|
|
63
|
-
}
|
|
59
|
+
});
|
|
64
60
|
|
|
65
61
|
// ========================================================================= //
|
|
66
62
|
// EXPORT //
|
|
67
63
|
// ========================================================================= //
|
|
68
64
|
|
|
69
|
-
export default
|
|
70
|
-
getAll,
|
|
71
|
-
addOne,
|
|
72
|
-
updateOne,
|
|
73
|
-
deleteOne,
|
|
74
|
-
} as const;
|
|
65
|
+
export default router;
|
|
@@ -11,9 +11,9 @@ import path from 'path';
|
|
|
11
11
|
import { RouteError } from '@src/common/classes/route-errors';
|
|
12
12
|
import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
|
|
13
13
|
import Paths from '@src/common/constants/Paths';
|
|
14
|
-
import BaseRouter from '@src/
|
|
14
|
+
import BaseRouter from '@src/routers/api.router';
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { Env, NodeEnvs } from './common/constants/env.constants';
|
|
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 (
|
|
31
|
+
if (Env.NODE_ENV === NodeEnvs.DEVELOPMENT) {
|
|
32
32
|
app.use(morgan('dev'));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Security
|
|
36
|
-
if (
|
|
36
|
+
if (Env.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,20 @@ 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
|
-
|
|
51
|
+
// Body-parser errors are client errors; don't expose the rejected body.
|
|
52
|
+
if ('type' in err) {
|
|
53
|
+
if (err.type === 'entity.parse.failed') {
|
|
54
|
+
return res
|
|
55
|
+
.status(HttpStatusCodes.BAD_REQUEST)
|
|
56
|
+
.json({ error: 'Invalid JSON' });
|
|
57
|
+
}
|
|
58
|
+
if (err.type === 'entity.too.large') {
|
|
59
|
+
return res
|
|
60
|
+
.status(HttpStatusCodes.PAYLOAD_TOO_LARGE)
|
|
61
|
+
.json({ error: 'Request body too large' });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (Env.NODE_ENV !== NodeEnvs.TEST) {
|
|
52
65
|
logger.err(err, true);
|
|
53
66
|
}
|
|
54
67
|
return res
|
|
@@ -64,7 +77,7 @@ const staticDir = path.join(import.meta.dirname, 'public');
|
|
|
64
77
|
|
|
65
78
|
// Refresh the browser on changes. Imported lazily because livereload is a
|
|
66
79
|
// dev dependency and isn't installed in production.
|
|
67
|
-
if (
|
|
80
|
+
if (Env.NODE_ENV === NodeEnvs.DEVELOPMENT) {
|
|
68
81
|
const { setupLiveReload } = await import('@src/common/utils/dev-only');
|
|
69
82
|
setupLiveReload(app, [staticDir, viewsDir]);
|
|
70
83
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<!-- Head -->
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
7
|
<title>Users</title>
|
|
7
8
|
<link rel="stylesheet" type="text/css" href="stylesheets/users.css" />
|
|
8
9
|
|
|
@@ -50,6 +51,13 @@
|
|
|
50
51
|
<div id="all-users-anchor"></div>
|
|
51
52
|
</div>
|
|
52
53
|
|
|
54
|
+
<!-- Error Display -->
|
|
55
|
+
<aside class="users-column errors-column" aria-labelledby="errors-heading">
|
|
56
|
+
<div id="errors-heading" class="column-header">Error Messages:</div>
|
|
57
|
+
<pre id="error-display" role="status" aria-live="polite">
|
|
58
|
+
No errors to display.</pre>
|
|
59
|
+
</aside>
|
|
60
|
+
|
|
53
61
|
<script src="/scripts/HttpClient.js"></script>
|
|
54
62
|
<script src="/scripts/renderUsers.js"></script>
|
|
55
63
|
<script src="/scripts/users.js"></script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Response } from 'supertest';
|
|
2
|
+
|
|
3
|
+
// ========================================================================= //
|
|
4
|
+
// TYPES //
|
|
5
|
+
// ========================================================================= //
|
|
6
|
+
|
|
7
|
+
export type TypedRes<T> = Omit<Response, 'body'> & { body: T };
|
|
8
|
+
export type TypedErrRes<T = 'string'> = TypedRes<{ error: T }>;
|
|
9
|
+
|
|
10
|
+
interface ValidationError {
|
|
11
|
+
errors: { message: string }[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ========================================================================= //
|
|
15
|
+
// HELPERS //
|
|
16
|
+
// ========================================================================= //
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the error message from a superTest Response object.
|
|
20
|
+
*/
|
|
21
|
+
export function extractErrMsg(res: TypedRes<ValidationError[]>) {
|
|
22
|
+
return res.body[0].errors[0].message;
|
|
23
|
+
}
|
|
@@ -1,28 +1,47 @@
|
|
|
1
|
-
import jetid from 'jet-id';
|
|
2
|
-
|
|
3
|
-
import { ValidationError } from '@src/common/classes/route-errors';
|
|
4
1
|
import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
|
|
5
2
|
import Paths from '@src/common/constants/Paths';
|
|
6
3
|
import User, { type UserEntity } from '@src/entities/User';
|
|
7
4
|
import UserRepo, { UserRepoTestOnly } from '@src/repos/UserRepo';
|
|
8
5
|
import UserService from '@src/services/UserService';
|
|
9
6
|
|
|
10
|
-
import { compareUserArrays } from './common/comparators';
|
|
11
|
-
import type { TestRes } from './common/supertest-types';
|
|
12
7
|
import { agent } from './support/agent';
|
|
8
|
+
import {
|
|
9
|
+
extractErrMsg,
|
|
10
|
+
type TypedErrRes,
|
|
11
|
+
type TypedRes,
|
|
12
|
+
} from './support/utils';
|
|
13
13
|
|
|
14
14
|
// ========================================================================= //
|
|
15
15
|
// CONSTANTS //
|
|
16
16
|
// ========================================================================= //
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// Shorthand the `Paths`
|
|
19
|
+
const P = {
|
|
20
|
+
Get: {
|
|
21
|
+
CALL: Paths.Users.Get.FULL_PATH,
|
|
22
|
+
DISPLAY: Paths.Users.Get.DISPLAY,
|
|
23
|
+
},
|
|
24
|
+
Add: {
|
|
25
|
+
CALL: Paths.Users.Add.FULL_PATH,
|
|
26
|
+
DISPLAY: Paths.Users.Add.DISPLAY,
|
|
27
|
+
},
|
|
28
|
+
Update: {
|
|
29
|
+
CALL: Paths.Users.Update.FULL_PATH,
|
|
30
|
+
DISPLAY: Paths.Users.Update.DISPLAY,
|
|
31
|
+
},
|
|
32
|
+
Delete: {
|
|
33
|
+
CALL: Paths.Users.Delete.format,
|
|
34
|
+
DISPLAY: Paths.Users.Delete.DISPLAY,
|
|
35
|
+
FULL_PATH: Paths.Users.Delete.FULL_PATH,
|
|
36
|
+
},
|
|
37
|
+
} as const;
|
|
23
38
|
|
|
24
39
|
// Errors
|
|
25
|
-
const
|
|
40
|
+
const Errors = {
|
|
41
|
+
...UserService.Errors,
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
// Status codes
|
|
26
45
|
const { BAD_REQUEST, CREATED, INTERNAL_SERVER_ERROR, OK, NOT_FOUND } =
|
|
27
46
|
HttpStatusCodes;
|
|
28
47
|
|
|
@@ -51,15 +70,15 @@ describe('UserRouter', () => {
|
|
|
51
70
|
vi.restoreAllMocks();
|
|
52
71
|
});
|
|
53
72
|
|
|
54
|
-
//
|
|
55
|
-
describe(
|
|
73
|
+
// --- `Get` ---
|
|
74
|
+
describe(P.Get.DISPLAY, () => {
|
|
56
75
|
describe('on success', () => {
|
|
57
76
|
it(`returns all users as JSON with status "${OK}"`, async () => {
|
|
58
|
-
const res:
|
|
59
|
-
|
|
77
|
+
const res: TypedRes<{ users: UserEntity[] }> = await agent.get(
|
|
78
|
+
P.Get.CALL,
|
|
79
|
+
);
|
|
60
80
|
expect(res.status).toBe(OK);
|
|
61
|
-
|
|
62
|
-
expect(usersMatch).toBeTruthy();
|
|
81
|
+
expect(res.body.users).toStrictEqual(DUMMY_USERS);
|
|
63
82
|
});
|
|
64
83
|
});
|
|
65
84
|
|
|
@@ -68,24 +87,24 @@ describe('UserRouter', () => {
|
|
|
68
87
|
const error = new Error('boom');
|
|
69
88
|
const getAllSpy = vi.spyOn(UserService, 'getAll');
|
|
70
89
|
getAllSpy.mockRejectedValueOnce(error);
|
|
71
|
-
const res:
|
|
90
|
+
const res: TypedErrRes = await agent.get(P.Get.CALL);
|
|
72
91
|
expect(res.status).toBe(INTERNAL_SERVER_ERROR);
|
|
73
92
|
expect(res.body.error).toBe('Internal Server Error');
|
|
74
93
|
});
|
|
75
94
|
});
|
|
76
95
|
});
|
|
77
96
|
|
|
78
|
-
//
|
|
79
|
-
describe(
|
|
97
|
+
// --- `Add` ---
|
|
98
|
+
describe(P.Add.DISPLAY, () => {
|
|
80
99
|
describe('on success', () => {
|
|
81
100
|
it(`persists the user and returns status "${CREATED}"`, async () => {
|
|
82
101
|
// Same payload shape the front-end sends
|
|
83
102
|
const input = { name: 'a', email: 'a@a.com' };
|
|
84
|
-
const res:
|
|
85
|
-
.post(
|
|
103
|
+
const res: TypedRes<{ user: UserEntity }> = await agent
|
|
104
|
+
.post(P.Add.CALL)
|
|
86
105
|
.send({ user: input });
|
|
87
106
|
expect(res.status).toBe(CREATED);
|
|
88
|
-
const isId = User.
|
|
107
|
+
const isId = User.is.id(res.body.user.id);
|
|
89
108
|
expect(isId).toBe(true);
|
|
90
109
|
const all = await UserRepo.getAll();
|
|
91
110
|
expect(all).toHaveLength(DUMMY_USERS.length + 1);
|
|
@@ -95,33 +114,63 @@ describe('UserRouter', () => {
|
|
|
95
114
|
|
|
96
115
|
describe('when the user param is missing', () => {
|
|
97
116
|
it('returns a validation error', async () => {
|
|
98
|
-
const res
|
|
99
|
-
.post(PATH_USERS_ADD)
|
|
100
|
-
.send({ user: null });
|
|
117
|
+
const res = await agent.post(P.Add.CALL).send({ user: null });
|
|
101
118
|
expect(res.status).toBe(BAD_REQUEST);
|
|
102
|
-
|
|
103
|
-
expect(
|
|
119
|
+
const errMsg = extractErrMsg(res);
|
|
120
|
+
expect(errMsg).toBe('Invalid input: expected object, received null');
|
|
104
121
|
});
|
|
105
122
|
});
|
|
106
123
|
|
|
107
124
|
describe('when the name is empty', () => {
|
|
108
125
|
it(`returns status "${BAD_REQUEST}"`, async () => {
|
|
109
|
-
const res
|
|
110
|
-
.post(
|
|
126
|
+
const res = await agent
|
|
127
|
+
.post(P.Add.CALL)
|
|
111
128
|
.send({ user: { name: '', email: 'a@a.com' } });
|
|
112
129
|
expect(res.status).toBe(BAD_REQUEST);
|
|
130
|
+
// Verify it didn't persist in the database
|
|
113
131
|
const allUsers = await UserRepo.getAll();
|
|
114
132
|
expect(allUsers).toHaveLength(DUMMY_USERS.length);
|
|
115
133
|
});
|
|
116
134
|
});
|
|
135
|
+
|
|
136
|
+
describe('when the request body cannot be parsed', () => {
|
|
137
|
+
it('returns 400 for malformed JSON without persisting a user', async () => {
|
|
138
|
+
const res: TypedErrRes = await agent
|
|
139
|
+
.post(P.Add.CALL)
|
|
140
|
+
.set('Content-Type', 'application/json')
|
|
141
|
+
.send('{"user":');
|
|
142
|
+
expect(res.status).toBe(BAD_REQUEST);
|
|
143
|
+
expect(res.body).toStrictEqual({ error: 'Invalid JSON' });
|
|
144
|
+
expect(await UserRepo.getAll()).toStrictEqual(DUMMY_USERS);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it.each([
|
|
148
|
+
[
|
|
149
|
+
'application/json',
|
|
150
|
+
JSON.stringify({ user: { name: 'a'.repeat(110000) } }),
|
|
151
|
+
],
|
|
152
|
+
[
|
|
153
|
+
'application/x-www-form-urlencoded',
|
|
154
|
+
`user[name]=${'a'.repeat(110000)}`,
|
|
155
|
+
],
|
|
156
|
+
])('returns 413 for an oversized %s body', async (contentType, body) => {
|
|
157
|
+
const res: TypedErrRes = await agent
|
|
158
|
+
.post(P.Add.CALL)
|
|
159
|
+
.set('Content-Type', contentType)
|
|
160
|
+
.send(body);
|
|
161
|
+
expect(res.status).toBe(HttpStatusCodes.PAYLOAD_TOO_LARGE);
|
|
162
|
+
expect(res.body).toStrictEqual({ error: 'Request body too large' });
|
|
163
|
+
expect(await UserRepo.getAll()).toStrictEqual(DUMMY_USERS);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
117
166
|
});
|
|
118
167
|
|
|
119
|
-
//
|
|
120
|
-
describe(
|
|
168
|
+
// --- `Update` ---
|
|
169
|
+
describe(P.Update.DISPLAY, () => {
|
|
121
170
|
describe('on success', () => {
|
|
122
171
|
it(`saves the change and returns status "${OK}"`, async () => {
|
|
123
172
|
const user = { ...dbUsers[0], name: 'Bill' };
|
|
124
|
-
const res = await agent.put(
|
|
173
|
+
const res = await agent.put(P.Update.CALL).send({ user });
|
|
125
174
|
expect(res.status).toBe(OK);
|
|
126
175
|
const allUsers = await UserRepo.getAll();
|
|
127
176
|
const saved = allUsers.find((u) => u.id === user.id);
|
|
@@ -138,30 +187,29 @@ describe('UserRouter', () => {
|
|
|
138
187
|
email: 'a@a.com',
|
|
139
188
|
id: '5',
|
|
140
189
|
};
|
|
141
|
-
const res
|
|
190
|
+
const res = await agent.put(P.Update.CALL).send({ user });
|
|
142
191
|
expect(res.status).toBe(BAD_REQUEST);
|
|
143
|
-
|
|
144
|
-
expect(
|
|
192
|
+
const errMsg = extractErrMsg(res);
|
|
193
|
+
expect(errMsg).toBe(errMsg);
|
|
145
194
|
});
|
|
146
195
|
});
|
|
147
196
|
|
|
148
197
|
describe('when the user is not found', () => {
|
|
149
198
|
it(`returns status "${NOT_FOUND}"`, async () => {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const res: TestRes = await agent.put(PATH_USERS_UPDATE).send({ user });
|
|
199
|
+
const user = User.create({ name: 'a', email: 'a@a.com' });
|
|
200
|
+
const res: TypedErrRes = await agent.put(P.Update.CALL).send({ user });
|
|
153
201
|
expect(res.status).toBe(NOT_FOUND);
|
|
154
|
-
expect(res.body.error).toBe(
|
|
202
|
+
expect(res.body.error).toBe(Errors.USER_NOT_FOUND);
|
|
155
203
|
});
|
|
156
204
|
});
|
|
157
205
|
});
|
|
158
206
|
|
|
159
|
-
//
|
|
160
|
-
describe(
|
|
207
|
+
// --- `Delete` ---
|
|
208
|
+
describe(P.Delete.DISPLAY, () => {
|
|
161
209
|
describe('on success', () => {
|
|
162
210
|
it(`removes the user and returns status "${OK}"`, async () => {
|
|
163
211
|
const id = dbUsers[0].id;
|
|
164
|
-
const path =
|
|
212
|
+
const path = P.Delete.CALL(id);
|
|
165
213
|
const res = await agent.delete(path);
|
|
166
214
|
expect(res.status).toBe(OK);
|
|
167
215
|
const persists = await UserRepo.persists(id);
|
|
@@ -171,17 +219,17 @@ describe('UserRouter', () => {
|
|
|
171
219
|
|
|
172
220
|
describe('when the user is not found', () => {
|
|
173
221
|
it(`returns status "${NOT_FOUND}"`, async () => {
|
|
174
|
-
const id =
|
|
175
|
-
const path =
|
|
176
|
-
const res:
|
|
222
|
+
const id = User.genId();
|
|
223
|
+
const path = P.Delete.CALL(id);
|
|
224
|
+
const res: TypedErrRes = await agent.delete(path);
|
|
177
225
|
expect(res.status).toBe(NOT_FOUND);
|
|
178
|
-
expect(res.body.error).toBe(
|
|
226
|
+
expect(res.body.error).toBe(Errors.USER_NOT_FOUND);
|
|
179
227
|
});
|
|
180
228
|
});
|
|
181
229
|
|
|
182
230
|
describe('when the id is invalid', () => {
|
|
183
231
|
it(`returns status "${BAD_REQUEST}"`, async () => {
|
|
184
|
-
const path =
|
|
232
|
+
const path = P.Delete.CALL('asdf');
|
|
185
233
|
const res = await agent.delete(path);
|
|
186
234
|
expect(res.status).toBe(BAD_REQUEST);
|
|
187
235
|
});
|
|
@@ -189,9 +237,10 @@ describe('UserRouter', () => {
|
|
|
189
237
|
|
|
190
238
|
describe('when the id path param is missing', () => {
|
|
191
239
|
it(`returns status "${BAD_REQUEST}"`, async () => {
|
|
192
|
-
const res
|
|
240
|
+
const res = await agent.delete(P.Delete.FULL_PATH);
|
|
193
241
|
expect(res.status).toBe(BAD_REQUEST);
|
|
194
|
-
|
|
242
|
+
const errMsg = extractErrMsg(res);
|
|
243
|
+
expect(errMsg).toStrictEqual('Not a valid uuid7 id');
|
|
195
244
|
});
|
|
196
245
|
});
|
|
197
246
|
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-generator-typescript",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Generate new Express applications similar to express-generate which but sets it up to use TypeScript instead",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"commit": "git add -A && git commit -m \"dev commit\"",
|
|
7
|
-
"commit:push": "npm run commit && git push",
|
|
8
6
|
"install:clean": "rm -rf node_modules && rm -rf package-lock.json && npm i",
|
|
9
7
|
"test": "node --test \"tests/**/*.test.js\"",
|
|
10
|
-
"test:e2e": "node scripts/e2e.js"
|
|
8
|
+
"test:e2e": "node scripts/e2e.js",
|
|
9
|
+
"_local:commit": "tsx --tsconfig ../my-tools/tsconfig.json ../my-tools/src/workflows/commit/run.ts",
|
|
10
|
+
"_local:commit:push": "npm run _local:commit -- -p",
|
|
11
|
+
"_local:squash-history": "tsx --tsconfig ../my-tools/tsconfig.json ../my-tools/src/workflows/squash-history/run.ts"
|
|
11
12
|
},
|
|
12
13
|
"main": "lib/express-generator-typescript.js",
|
|
13
14
|
"bin": {
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
"!lib/template/src/repos/common/database.test.json"
|
|
26
27
|
],
|
|
27
28
|
"engines": {
|
|
28
|
-
"node": "
|
|
29
|
+
"node": "^22.13.0 || ^24.0.0 || >=26.0.0"
|
|
29
30
|
},
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import jetPaths from 'jet-paths';
|
|
2
|
-
|
|
3
|
-
// ========================================================================= //
|
|
4
|
-
// EXPORT //
|
|
5
|
-
// ========================================================================= //
|
|
6
|
-
|
|
7
|
-
export default jetPaths({
|
|
8
|
-
$path: '/api',
|
|
9
|
-
Users: {
|
|
10
|
-
$path: '/users',
|
|
11
|
-
Get: '/all',
|
|
12
|
-
Add: '/add',
|
|
13
|
-
Update: '/update',
|
|
14
|
-
Delete: '/delete/:id',
|
|
15
|
-
},
|
|
16
|
-
} as const);
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import jetid from 'jet-id';
|
|
2
|
-
import { isNonEmptyString, isString } from 'jet-validators';
|
|
3
|
-
import { parseObject, type Schema, testObject } from 'jet-validators/utils';
|
|
4
|
-
|
|
5
|
-
import { isISOString } from '@src/common/utils/date-utils';
|
|
6
|
-
|
|
7
|
-
import type { UserEntity, UserInput } from '../types';
|
|
8
|
-
|
|
9
|
-
// ========================================================================= //
|
|
10
|
-
// FUNCTIONS //
|
|
11
|
-
// ========================================================================= //
|
|
12
|
-
|
|
13
|
-
const schema: Schema<UserEntity> = {
|
|
14
|
-
id: isUserId,
|
|
15
|
-
name: isString,
|
|
16
|
-
email: isString,
|
|
17
|
-
created: isISOString,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Validate the `User` schema.
|
|
22
|
-
*/
|
|
23
|
-
export const parseUser = parseObject<UserEntity>(schema);
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* For the APIs make sure the right fields are complete
|
|
27
|
-
*/
|
|
28
|
-
export const isCompleteUser = testObject<UserEntity>({
|
|
29
|
-
...schema,
|
|
30
|
-
name: isNonEmptyString,
|
|
31
|
-
email: isNonEmptyString,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Validate the fields a client sends to create a user.
|
|
36
|
-
*/
|
|
37
|
-
export const isUserInput = testObject<UserInput>({
|
|
38
|
-
name: isNonEmptyString,
|
|
39
|
-
email: isNonEmptyString,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Test if an id is a valid user id.
|
|
44
|
-
*/
|
|
45
|
-
export function isUserId(val: unknown): val is string {
|
|
46
|
-
return jetid.test(val);
|
|
47
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { Request, Response } from 'express';
|
|
2
|
-
|
|
3
|
-
// ========================================================================= //
|
|
4
|
-
// TYPES //
|
|
5
|
-
// ========================================================================= //
|
|
6
|
-
|
|
7
|
-
// Body is `unknown` until it has been validated with `parseReq`.
|
|
8
|
-
export type Req = Request<Record<string, string>, unknown, unknown>;
|
|
9
|
-
export type Res = Response;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { parseObject, type Schema } from 'jet-validators/utils';
|
|
2
|
-
|
|
3
|
-
import { ValidationError } from '@src/common/classes/route-errors';
|
|
4
|
-
|
|
5
|
-
// ========================================================================= //
|
|
6
|
-
// FUNCTIONS //
|
|
7
|
-
// ========================================================================= //
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Build a request parser for `schema` that throws a `ValidationError`
|
|
11
|
-
* (400) listing every failed field.
|
|
12
|
-
*/
|
|
13
|
-
function parseReq<U extends Schema>(schema: U) {
|
|
14
|
-
return parseObject(schema, (errors) => {
|
|
15
|
-
throw new ValidationError(errors);
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// ========================================================================= //
|
|
20
|
-
// EXPORT //
|
|
21
|
-
// ========================================================================= //
|
|
22
|
-
|
|
23
|
-
export default parseReq;
|