express-generator-typescript 3.0.4 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -11
- package/bin/cli.js +1 -20
- package/lib/template/.prettierrc.json +1 -1
- package/lib/template/README.md +13 -15
- package/lib/template/eslint.config.ts +10 -1
- package/lib/template/gitignore +0 -1
- package/lib/template/package.json +19 -29
- package/lib/template/src/common/classes/route-errors.ts +1 -14
- package/lib/template/src/common/constants/HttpStatusCodes.ts +1 -1
- 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/dev-only.ts +1 -1
- package/lib/template/src/common/utils/validators.ts +0 -23
- package/lib/template/src/entities/User/index.ts +2 -2
- package/lib/template/src/entities/User/module.ts +77 -28
- package/lib/template/src/entities/User/types.ts +1 -1
- package/lib/template/src/entities/common/entity-base-types.ts +1 -1
- package/lib/template/src/main.ts +4 -4
- 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 +6 -6
- package/lib/template/src/repos/UserRepo.ts +6 -16
- package/lib/template/src/repos/common/database.json +3 -3
- package/lib/template/src/{routes/apiRouter.ts → routers/api.router.ts} +4 -12
- package/lib/template/src/routers/user.router.ts +65 -0
- package/lib/template/src/server.ts +24 -11
- package/lib/template/src/services/UserService.ts +6 -3
- package/lib/template/src/views/users.html +8 -0
- package/lib/template/tests/frontend.test.ts +28 -17
- package/lib/template/tests/support/agent.ts +4 -4
- package/lib/template/tests/support/utils.ts +23 -0
- package/lib/template/tests/users.test.ts +136 -83
- package/lib/template/tsconfig.json +10 -13
- package/package.json +6 -6
- 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/UserRoutes.ts +0 -74
- 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
- package/lib/template/tsconfig.prod.json +0 -30
- package/lib/template/vitest.config.ts +0 -25
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
+
import HttpStatusCodes from '#src/common/constants/HttpStatusCodes.ts';
|
|
5
|
+
|
|
6
|
+
import { agent } from './support/agent.ts';
|
|
7
|
+
|
|
8
|
+
// ========================================================================= //
|
|
9
|
+
// CONSTANTS //
|
|
10
|
+
// ========================================================================= //
|
|
11
|
+
|
|
12
|
+
const STATIC_ASSETS = [
|
|
13
|
+
'/scripts/HttpClient.js',
|
|
14
|
+
'/scripts/renderUsers.js',
|
|
15
|
+
'/scripts/users.js',
|
|
16
|
+
'/scripts/lib/bootstrap.bundle.min.js',
|
|
17
|
+
'/stylesheets/users.css',
|
|
18
|
+
'/stylesheets/lib/bootstrap.min.css',
|
|
19
|
+
] as const;
|
|
4
20
|
|
|
5
21
|
// ========================================================================= //
|
|
6
22
|
// TESTS //
|
|
@@ -9,25 +25,20 @@ import { agent } from './support/agent';
|
|
|
9
25
|
describe('Front-end content', () => {
|
|
10
26
|
it('should redirect "/" to the users page.', async () => {
|
|
11
27
|
const res = await agent.get('/');
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
assert.equal(res.status, HttpStatusCodes.FOUND);
|
|
29
|
+
assert.equal(res.headers.location, '/users');
|
|
14
30
|
});
|
|
15
31
|
|
|
16
32
|
it('should serve the users page.', async () => {
|
|
17
33
|
const res = await agent.get('/users');
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
assert.equal(res.status, HttpStatusCodes.OK);
|
|
35
|
+
assert.match(res.headers['content-type'], /html/);
|
|
20
36
|
});
|
|
21
37
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'/stylesheets/lib/bootstrap.min.css',
|
|
29
|
-
])('should serve the static asset "%s".', async (asset) => {
|
|
30
|
-
const res = await agent.get(asset);
|
|
31
|
-
expect(res.status).toBe(HttpStatusCodes.OK);
|
|
32
|
-
});
|
|
38
|
+
for (const asset of STATIC_ASSETS) {
|
|
39
|
+
it(`should serve the static asset "${asset}".`, async () => {
|
|
40
|
+
const res = await agent.get(asset);
|
|
41
|
+
assert.equal(res.status, HttpStatusCodes.OK);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
33
44
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { before } from 'node:test';
|
|
1
2
|
import supertest, { type Test } from 'supertest';
|
|
2
3
|
import type TestAgent from 'supertest/lib/agent.js';
|
|
3
|
-
import { beforeAll } from 'vitest';
|
|
4
4
|
|
|
5
|
-
import MockOrm from '
|
|
6
|
-
import app from '
|
|
5
|
+
import MockOrm from '#src/repos/MockOrm.ts';
|
|
6
|
+
import app from '#src/server.ts';
|
|
7
7
|
|
|
8
8
|
// ========================================================================= //
|
|
9
9
|
// EXEC //
|
|
@@ -11,7 +11,7 @@ import app from '@src/server';
|
|
|
11
11
|
|
|
12
12
|
let agent: TestAgent<Test>;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
before(async () => {
|
|
15
15
|
agent = supertest.agent(app);
|
|
16
16
|
await MockOrm.cleanDb();
|
|
17
17
|
});
|
|
@@ -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,50 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import HttpStatusCodes from '
|
|
5
|
-
import Paths from '
|
|
6
|
-
import User, { type UserEntity } from '
|
|
7
|
-
import UserRepo, { UserRepoTestOnly } from '
|
|
8
|
-
import UserService from '
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { beforeEach, describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import HttpStatusCodes from '#src/common/constants/HttpStatusCodes.ts';
|
|
5
|
+
import Paths from '#src/common/constants/Paths/index.ts';
|
|
6
|
+
import User, { type UserEntity } from '#src/entities/User/index.ts';
|
|
7
|
+
import UserRepo, { UserRepoTestOnly } from '#src/repos/UserRepo.ts';
|
|
8
|
+
import UserService from '#src/services/UserService.ts';
|
|
9
|
+
|
|
10
|
+
import { agent } from './support/agent.ts';
|
|
11
|
+
import {
|
|
12
|
+
extractErrMsg,
|
|
13
|
+
type TypedErrRes,
|
|
14
|
+
type TypedRes,
|
|
15
|
+
} from './support/utils.ts';
|
|
13
16
|
|
|
14
17
|
// ========================================================================= //
|
|
15
18
|
// CONSTANTS //
|
|
16
19
|
// ========================================================================= //
|
|
17
20
|
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Shorthand the `Paths`
|
|
22
|
+
const P = {
|
|
23
|
+
Get: {
|
|
24
|
+
CALL: Paths.Users.Get.FULL_PATH,
|
|
25
|
+
DISPLAY: Paths.Users.Get.DISPLAY,
|
|
26
|
+
},
|
|
27
|
+
Add: {
|
|
28
|
+
CALL: Paths.Users.Add.FULL_PATH,
|
|
29
|
+
DISPLAY: Paths.Users.Add.DISPLAY,
|
|
30
|
+
},
|
|
31
|
+
Update: {
|
|
32
|
+
CALL: Paths.Users.Update.FULL_PATH,
|
|
33
|
+
DISPLAY: Paths.Users.Update.DISPLAY,
|
|
34
|
+
},
|
|
35
|
+
Delete: {
|
|
36
|
+
CALL: Paths.Users.Delete.format,
|
|
37
|
+
DISPLAY: Paths.Users.Delete.DISPLAY,
|
|
38
|
+
FULL_PATH: Paths.Users.Delete.FULL_PATH,
|
|
39
|
+
},
|
|
40
|
+
} as const;
|
|
23
41
|
|
|
24
42
|
// Errors
|
|
25
|
-
const
|
|
43
|
+
const Errors = {
|
|
44
|
+
...UserService.Errors,
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
// Status codes
|
|
26
48
|
const { BAD_REQUEST, CREATED, INTERNAL_SERVER_ERROR, OK, NOT_FOUND } =
|
|
27
49
|
HttpStatusCodes;
|
|
28
50
|
|
|
@@ -47,85 +69,116 @@ describe('UserRouter', () => {
|
|
|
47
69
|
dbUsers = await UserRepoTestOnly.insertMultiple(DUMMY_USERS);
|
|
48
70
|
});
|
|
49
71
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// ---- `Get`
|
|
55
|
-
describe(`"GET - ${PATH_USERS_GET}"`, () => {
|
|
72
|
+
// --- `Get` ---
|
|
73
|
+
describe(P.Get.DISPLAY, () => {
|
|
56
74
|
describe('on success', () => {
|
|
57
75
|
it(`returns all users as JSON with status "${OK}"`, async () => {
|
|
58
|
-
const res:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
const res: TypedRes<{ users: UserEntity[] }> = await agent.get(
|
|
77
|
+
P.Get.CALL,
|
|
78
|
+
);
|
|
79
|
+
assert.equal(res.status, OK);
|
|
80
|
+
assert.deepEqual(res.body.users, DUMMY_USERS);
|
|
63
81
|
});
|
|
64
82
|
});
|
|
65
83
|
|
|
66
84
|
describe('when an unexpected error is thrown', () => {
|
|
67
|
-
it(`returns status "${INTERNAL_SERVER_ERROR}"`, async () => {
|
|
85
|
+
it(`returns status "${INTERNAL_SERVER_ERROR}"`, async (t) => {
|
|
68
86
|
const error = new Error('boom');
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const res:
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
// Mocks made through `t.mock` are restored when the test ends
|
|
88
|
+
t.mock.method(UserService, 'getAll', () => Promise.reject(error));
|
|
89
|
+
const res: TypedErrRes = await agent.get(P.Get.CALL);
|
|
90
|
+
assert.equal(res.status, INTERNAL_SERVER_ERROR);
|
|
91
|
+
assert.equal(res.body.error, 'Internal Server Error');
|
|
74
92
|
});
|
|
75
93
|
});
|
|
76
94
|
});
|
|
77
95
|
|
|
78
|
-
//
|
|
79
|
-
describe(
|
|
96
|
+
// --- `Add` ---
|
|
97
|
+
describe(P.Add.DISPLAY, () => {
|
|
80
98
|
describe('on success', () => {
|
|
81
99
|
it(`persists the user and returns status "${CREATED}"`, async () => {
|
|
82
100
|
// Same payload shape the front-end sends
|
|
83
101
|
const input = { name: 'a', email: 'a@a.com' };
|
|
84
|
-
const res:
|
|
85
|
-
.post(
|
|
102
|
+
const res: TypedRes<{ user: UserEntity }> = await agent
|
|
103
|
+
.post(P.Add.CALL)
|
|
86
104
|
.send({ user: input });
|
|
87
|
-
|
|
88
|
-
const isId = User.
|
|
89
|
-
|
|
105
|
+
assert.equal(res.status, CREATED);
|
|
106
|
+
const isId = User.is.id(res.body.user.id);
|
|
107
|
+
assert.equal(isId, true);
|
|
90
108
|
const all = await UserRepo.getAll();
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
assert.equal(all.length, DUMMY_USERS.length + 1);
|
|
110
|
+
const saved = all.find((u) => u.id === res.body.user.id);
|
|
111
|
+
assert.deepEqual(saved, res.body.user);
|
|
93
112
|
});
|
|
94
113
|
});
|
|
95
114
|
|
|
96
115
|
describe('when the user param is missing', () => {
|
|
97
116
|
it('returns a validation error', async () => {
|
|
98
|
-
const res
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
expect(res.body.error).toBe(ValidationError.MESSAGE);
|
|
103
|
-
expect(res.body.errors?.[0].key).toStrictEqual('user');
|
|
117
|
+
const res = await agent.post(P.Add.CALL).send({ user: null });
|
|
118
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
119
|
+
const errMsg = extractErrMsg(res);
|
|
120
|
+
assert.equal(errMsg, '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
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
130
|
+
// Verify it didn't persist in the database
|
|
113
131
|
const allUsers = await UserRepo.getAll();
|
|
114
|
-
|
|
132
|
+
assert.equal(allUsers.length, 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
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
143
|
+
assert.deepEqual(res.body, { error: 'Invalid JSON' });
|
|
144
|
+
assert.deepEqual(await UserRepo.getAll(), DUMMY_USERS);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const oversizedBodies = [
|
|
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
|
+
] as const;
|
|
157
|
+
|
|
158
|
+
for (const [contentType, body] of oversizedBodies) {
|
|
159
|
+
it(`returns 413 for an oversized ${contentType} body`, async () => {
|
|
160
|
+
const res: TypedErrRes = await agent
|
|
161
|
+
.post(P.Add.CALL)
|
|
162
|
+
.set('Content-Type', contentType)
|
|
163
|
+
.send(body);
|
|
164
|
+
assert.equal(res.status, HttpStatusCodes.PAYLOAD_TOO_LARGE);
|
|
165
|
+
assert.deepEqual(res.body, { error: 'Request body too large' });
|
|
166
|
+
assert.deepEqual(await UserRepo.getAll(), DUMMY_USERS);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
117
170
|
});
|
|
118
171
|
|
|
119
|
-
//
|
|
120
|
-
describe(
|
|
172
|
+
// --- `Update` ---
|
|
173
|
+
describe(P.Update.DISPLAY, () => {
|
|
121
174
|
describe('on success', () => {
|
|
122
175
|
it(`saves the change and returns status "${OK}"`, async () => {
|
|
123
176
|
const user = { ...dbUsers[0], name: 'Bill' };
|
|
124
|
-
const res = await agent.put(
|
|
125
|
-
|
|
177
|
+
const res = await agent.put(P.Update.CALL).send({ user });
|
|
178
|
+
assert.equal(res.status, OK);
|
|
126
179
|
const allUsers = await UserRepo.getAll();
|
|
127
180
|
const saved = allUsers.find((u) => u.id === user.id);
|
|
128
|
-
|
|
181
|
+
assert.equal(saved?.name, 'Bill');
|
|
129
182
|
});
|
|
130
183
|
});
|
|
131
184
|
|
|
@@ -138,60 +191,60 @@ describe('UserRouter', () => {
|
|
|
138
191
|
email: 'a@a.com',
|
|
139
192
|
id: '5',
|
|
140
193
|
};
|
|
141
|
-
const res
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
194
|
+
const res = await agent.put(P.Update.CALL).send({ user });
|
|
195
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
196
|
+
const errMsg = extractErrMsg(res);
|
|
197
|
+
assert.equal(errMsg, 'Not a valid uuid7 id');
|
|
145
198
|
});
|
|
146
199
|
});
|
|
147
200
|
|
|
148
201
|
describe('when the user is not found', () => {
|
|
149
202
|
it(`returns status "${NOT_FOUND}"`, async () => {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
expect(res.body.error).toBe(ERR_USER_NOT_FOUND);
|
|
203
|
+
const user = User.create({ name: 'a', email: 'a@a.com' });
|
|
204
|
+
const res: TypedErrRes = await agent.put(P.Update.CALL).send({ user });
|
|
205
|
+
assert.equal(res.status, NOT_FOUND);
|
|
206
|
+
assert.equal(res.body.error, Errors.USER_NOT_FOUND);
|
|
155
207
|
});
|
|
156
208
|
});
|
|
157
209
|
});
|
|
158
210
|
|
|
159
|
-
//
|
|
160
|
-
describe(
|
|
211
|
+
// --- `Delete` ---
|
|
212
|
+
describe(P.Delete.DISPLAY, () => {
|
|
161
213
|
describe('on success', () => {
|
|
162
214
|
it(`removes the user and returns status "${OK}"`, async () => {
|
|
163
215
|
const id = dbUsers[0].id;
|
|
164
|
-
const path =
|
|
216
|
+
const path = P.Delete.CALL(id);
|
|
165
217
|
const res = await agent.delete(path);
|
|
166
|
-
|
|
218
|
+
assert.equal(res.status, OK);
|
|
167
219
|
const persists = await UserRepo.persists(id);
|
|
168
|
-
|
|
220
|
+
assert.equal(persists, false);
|
|
169
221
|
});
|
|
170
222
|
});
|
|
171
223
|
|
|
172
224
|
describe('when the user is not found', () => {
|
|
173
225
|
it(`returns status "${NOT_FOUND}"`, async () => {
|
|
174
|
-
const id =
|
|
175
|
-
const path =
|
|
176
|
-
const res:
|
|
177
|
-
|
|
178
|
-
|
|
226
|
+
const id = User.genId();
|
|
227
|
+
const path = P.Delete.CALL(id);
|
|
228
|
+
const res: TypedErrRes = await agent.delete(path);
|
|
229
|
+
assert.equal(res.status, NOT_FOUND);
|
|
230
|
+
assert.equal(res.body.error, Errors.USER_NOT_FOUND);
|
|
179
231
|
});
|
|
180
232
|
});
|
|
181
233
|
|
|
182
234
|
describe('when the id is invalid', () => {
|
|
183
235
|
it(`returns status "${BAD_REQUEST}"`, async () => {
|
|
184
|
-
const path =
|
|
236
|
+
const path = P.Delete.CALL('asdf');
|
|
185
237
|
const res = await agent.delete(path);
|
|
186
|
-
|
|
238
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
187
239
|
});
|
|
188
240
|
});
|
|
189
241
|
|
|
190
242
|
describe('when the id path param is missing', () => {
|
|
191
243
|
it(`returns status "${BAD_REQUEST}"`, async () => {
|
|
192
|
-
const res
|
|
193
|
-
|
|
194
|
-
|
|
244
|
+
const res = await agent.delete(P.Delete.FULL_PATH);
|
|
245
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
246
|
+
const errMsg = extractErrMsg(res);
|
|
247
|
+
assert.equal(errMsg, 'Not a valid uuid7 id');
|
|
195
248
|
});
|
|
196
249
|
});
|
|
197
250
|
});
|
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"module": "
|
|
5
|
-
"moduleResolution": "bundler",
|
|
4
|
+
"module": "nodenext",
|
|
6
5
|
"esModuleInterop": true,
|
|
7
6
|
"lib": ["ES2022"],
|
|
8
7
|
"strict": true,
|
|
9
8
|
"skipLibCheck": true,
|
|
10
9
|
"forceConsistentCasingInFileNames": true,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// Node runs the .ts files directly and tsc only typechecks, so imports
|
|
11
|
+
// name the .ts files
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
// Node strips types one file at a time, so type-only imports must say so
|
|
15
|
+
// and syntax that needs compiling (enums, namespaces...) isn't allowed
|
|
15
16
|
"verbatimModuleSyntax": true,
|
|
17
|
+
"erasableSyntaxOnly": true,
|
|
16
18
|
"useUnknownInCatchVariables": false,
|
|
17
|
-
"types": ["node"
|
|
19
|
+
"types": ["node"]
|
|
18
20
|
},
|
|
19
|
-
"include": [
|
|
20
|
-
"src/**/*.ts",
|
|
21
|
-
"tests/**/*.ts",
|
|
22
|
-
"eslint.config.ts",
|
|
23
|
-
"vitest.config.ts"
|
|
24
|
-
],
|
|
21
|
+
"include": ["src/**/*.ts", "tests/**/*.ts", "eslint.config.ts"],
|
|
25
22
|
"exclude": ["src/public/*"]
|
|
26
23
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-generator-typescript",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.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": "node ../my-tools/src/workflows/commit/run.ts",
|
|
10
|
+
"_local:commit:push": "npm run _local:commit -- -p",
|
|
11
|
+
"_local:squash-history": "node ../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.18.0 || ^24.0.0 || >=26.0.0"
|
|
29
30
|
},
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|
|
@@ -53,7 +54,6 @@
|
|
|
53
54
|
"express5",
|
|
54
55
|
"server",
|
|
55
56
|
"tooling",
|
|
56
|
-
"vitest",
|
|
57
57
|
"eslint"
|
|
58
58
|
],
|
|
59
59
|
"author": "sean maxwell",
|
|
@@ -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,74 +0,0 @@
|
|
|
1
|
-
import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
|
|
2
|
-
import User from '@src/entities/User';
|
|
3
|
-
import UserService from '@src/services/UserService';
|
|
4
|
-
|
|
5
|
-
import type { Req, Res } from './common/express-types';
|
|
6
|
-
import parseReq from './common/parseReq';
|
|
7
|
-
|
|
8
|
-
// ========================================================================= //
|
|
9
|
-
// EXEC //
|
|
10
|
-
// ========================================================================= //
|
|
11
|
-
|
|
12
|
-
const validators = {
|
|
13
|
-
add: parseReq({ user: User.isInput }),
|
|
14
|
-
update: parseReq({ user: User.isComplete }),
|
|
15
|
-
delete: parseReq({ id: User.isId }),
|
|
16
|
-
} as const;
|
|
17
|
-
|
|
18
|
-
// ========================================================================= //
|
|
19
|
-
// FUNCTIONS //
|
|
20
|
-
// ========================================================================= //
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get all users.
|
|
24
|
-
*
|
|
25
|
-
* @route GET /api/users/all
|
|
26
|
-
*/
|
|
27
|
-
async function getAll(_: Req, res: Res) {
|
|
28
|
-
const users = await UserService.getAll();
|
|
29
|
-
res.status(HttpStatusCodes.OK).json({ users });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Add one user.
|
|
34
|
-
*
|
|
35
|
-
* @route POST /api/users/add
|
|
36
|
-
*/
|
|
37
|
-
async function addOne(req: Req, res: Res) {
|
|
38
|
-
const { user } = validators.add(req.body);
|
|
39
|
-
const created = await UserService.addOne(user);
|
|
40
|
-
res.status(HttpStatusCodes.CREATED).json({ user: created });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Update one user.
|
|
45
|
-
*
|
|
46
|
-
* @route PUT /api/users/update
|
|
47
|
-
*/
|
|
48
|
-
async function updateOne(req: Req, res: Res) {
|
|
49
|
-
const { user } = validators.update(req.body);
|
|
50
|
-
await UserService.updateOne(user);
|
|
51
|
-
res.status(HttpStatusCodes.OK).end();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Delete one user.
|
|
56
|
-
*
|
|
57
|
-
* @route DELETE /api/users/delete/:id
|
|
58
|
-
*/
|
|
59
|
-
async function deleteOne(req: Req, res: Res) {
|
|
60
|
-
const { id } = validators.delete(req.params);
|
|
61
|
-
await UserService.deleteOne(id);
|
|
62
|
-
res.status(HttpStatusCodes.OK).end();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ========================================================================= //
|
|
66
|
-
// EXPORT //
|
|
67
|
-
// ========================================================================= //
|
|
68
|
-
|
|
69
|
-
export default {
|
|
70
|
-
getAll,
|
|
71
|
-
addOne,
|
|
72
|
-
updateOne,
|
|
73
|
-
deleteOne,
|
|
74
|
-
} as const;
|
|
@@ -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;
|