express-generator-typescript 2.5.12 → 2.5.15

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.
@@ -27,7 +27,7 @@
27
27
  "jet-logger": "^2.0.1",
28
28
  "jet-paths": "^1.1.0",
29
29
  "jet-schema": "^1.4.2",
30
- "jet-validators": "^1.0.9",
30
+ "jet-validators": "^1.0.17",
31
31
  "jsonfile": "^6.1.0",
32
32
  "module-alias": "^2.2.2",
33
33
  "morgan": "^1.10.0"
@@ -1,18 +1,26 @@
1
- import { isValidDate } from 'jet-validators';
2
- import { traverseObject } from 'jet-validators/utils';
1
+ import { IReqPropErr } from '@src/routes/common';
2
+ import UserRepo from '@src/repos/UserRepo';
3
3
 
4
4
 
5
+ /******************************************************************************
6
+ Types
7
+ ******************************************************************************/
8
+
9
+ export interface IValidationErr {
10
+ message: string;
11
+ parameters: IReqPropErr[];
12
+ }
13
+
14
+
15
+ /******************************************************************************
16
+ Functions
17
+ ******************************************************************************/
18
+
5
19
  /**
6
- * Send a date object over an API will convert it to a string, so for jasmine
7
- * tests to pass we need to convert them back to date objects.
20
+ * Delete all records for unit testing.
8
21
  */
9
- export function convertValidDates(toConvert: string | string[]) {
10
- if (!Array.isArray(toConvert)) {
11
- toConvert = [toConvert];
12
- }
13
- return traverseObject((prop, value, parentObj) => {
14
- if (toConvert.includes(prop) && isValidDate(value)) {
15
- parentObj[prop] = new Date(value);
16
- }
17
- });
22
+ export async function cleanDatabase(): Promise<void> {
23
+ await Promise.all([
24
+ UserRepo.deleteAllUsers(),
25
+ ]);
18
26
  }
@@ -1,8 +1,6 @@
1
- import supertest, { Test } from 'supertest';
2
- import TestAgent from 'supertest/lib/agent';
1
+ import supertest from 'supertest';
3
2
  import insertUrlParams from 'inserturlparams';
4
- import { parseJson } from 'jet-validators/utils';
5
- import logger from 'jet-logger';
3
+ import { parseJson, customDeepCompare } from 'jet-validators/utils';
6
4
 
7
5
  import app from '@src/server';
8
6
 
@@ -11,191 +9,174 @@ import User, { IUser } from '@src/models/User';
11
9
  import { USER_NOT_FOUND_ERR } from '@src/services/UserService';
12
10
 
13
11
  import HttpStatusCodes from '@src/common/HttpStatusCodes';
14
- import { IValidationErrFormat, ValidationErr } from '@src/common/route-errors';
12
+ import { ValidationErr } from '@src/common/route-errors';
15
13
 
16
14
  import Paths from 'spec/support/Paths';
17
- import { convertValidDates } from 'spec/support';
18
- import { TApiCb, TRes } from 'spec/types/misc';
15
+ import { cleanDatabase, IValidationErr } from 'spec/support';
19
16
 
20
17
 
21
18
  /******************************************************************************
22
- Setup
19
+ Variables
23
20
  ******************************************************************************/
24
21
 
25
22
  // Dummy users for GET req
26
- const getDummyUsers = () => [
23
+ const DB_USERS = [
27
24
  User.new({ name: 'Sean Maxwell', email: 'sean.maxwell@gmail.com' }),
28
25
  User.new({ name: 'John Smith', email: 'john.smith@gmail.com' }),
29
26
  User.new({ name: 'Gordan Freeman', email: 'gordan.freeman@gmail.com' }),
30
- ];
27
+ ] as const;
31
28
 
32
- // Convert "created" prop back to Date object
33
- const createdKeyToDate = convertValidDates('created');
34
-
35
- // Setup wrap-callback function
36
- const wrapCb = (cb: TApiCb) => (err: Error, res: TRes) => {
37
- if (!!err) {
38
- logger.err(err);
39
- }
40
- createdKeyToDate(res.body);
41
- return cb(res);
42
- };
29
+ // Don't compare "id" and "created" cause those are set dynamically by the
30
+ // database
31
+ const compareUserArrays = customDeepCompare({
32
+ onlyCompareProps: ['name', 'email'],
33
+ })
43
34
 
44
35
 
45
36
  /******************************************************************************
46
37
  Tests
38
+ IMPORTANT: Following TypeScript best practices, we test all scenarios that
39
+ can be triggered by a user under normal circumstances. Not all theoretically
40
+ scenarios (i.e. a failed database connection).
47
41
  ******************************************************************************/
48
42
 
49
43
  describe('UserRouter', () => {
50
44
 
51
- let agent: TestAgent<Test>;
45
+ const agent = supertest.agent(app);
46
+ let dbUsers: IUser[] = [];
52
47
 
53
48
  // Run before all tests
54
- beforeAll(done => {
55
- agent = supertest.agent(app);
56
- done();
49
+ beforeEach(async () => {
50
+ await cleanDatabase();
51
+ dbUsers = await UserRepo.insertMult(DB_USERS);
57
52
  });
58
53
 
59
54
  // Get all users
60
55
  describe(`"GET:${Paths.Users.Get}"`, () => {
61
56
 
62
- // Setup API
63
- const api = (cb: TApiCb) =>
64
- agent
65
- .get(Paths.Users.Get)
66
- .end(wrapCb(cb));
67
-
68
57
  // Success
69
58
  it('should return a JSON object with all the users and a status code ' +
70
59
  `of "${HttpStatusCodes.OK}" if the request was successful.`, done => {
71
- // Add spy
72
- const data = getDummyUsers();
73
- spyOn(UserRepo, 'getAll').and.resolveTo(data);
74
- // Call API
75
- api(res => {
76
- expect(res.status).toBe(HttpStatusCodes.OK);
77
- expect(res.body).toEqual({ users: data });
78
- done();
79
- });
60
+ agent
61
+ .get(Paths.Users.Get)
62
+ .end((_, res) => {
63
+ expect(res.status).toBe(HttpStatusCodes.OK);
64
+ expect(compareUserArrays(res.body.users, DB_USERS)).toBeTruthy();
65
+ done();
66
+ });
80
67
  });
81
68
  });
82
69
 
83
70
  // Test add user
84
71
  describe(`"POST:${Paths.Users.Add}"`, () => {
85
72
 
86
- const DUMMY_USER = getDummyUsers()[0];
87
-
88
- // Setup API
89
- const callApi = (user: IUser | null, cb: TApiCb) =>
90
- agent
91
- .post(Paths.Users.Add)
92
- .send({ user })
93
- .end(wrapCb(cb));
94
-
95
73
  // Test add user success
96
74
  it(`should return a status code of "${HttpStatusCodes.CREATED}" if the ` +
97
75
  'request was successful.', done => {
98
- // Spy
99
- spyOn(UserRepo, 'add').and.resolveTo();
100
- // Call api
101
- callApi(DUMMY_USER, res => {
102
- expect(res.status).toBe(HttpStatusCodes.CREATED);
103
- done();
104
- });
76
+ const user = User.new({ name: 'a', email: 'a@a.com' });
77
+ agent
78
+ .post(Paths.Users.Add)
79
+ .send({ user })
80
+ .end((_, res) => {
81
+ expect(res.status).toBe(HttpStatusCodes.CREATED);
82
+ done();
83
+ });
105
84
  });
106
85
 
107
86
  // Missing param
108
87
  it('should return a JSON object with an error message of and a status ' +
109
88
  `code of "${HttpStatusCodes.BAD_REQUEST}" if the user param was ` +
110
89
  'missing.', done => {
111
- // Call api
112
- callApi(null, res => {
113
- expect(res.status).toBe(HttpStatusCodes.BAD_REQUEST);
114
- const errorObj = parseJson<IValidationErrFormat>(res.body.error);
115
- expect(errorObj.error).toBe(ValidationErr.MSG);
116
- expect(errorObj.parameter).toBe('user');
117
- done();
118
- });
90
+ agent
91
+ .post(Paths.Users.Add)
92
+ .send({ user: null })
93
+ .end((_, res) => {
94
+ expect(res.status).toBe(HttpStatusCodes.BAD_REQUEST);
95
+ const errorObj = parseJson<IValidationErr>(res.body.error);
96
+ expect(errorObj.message).toBe(ValidationErr.MSG);
97
+ expect(errorObj.parameters[0].prop).toBe('user');
98
+ done();
99
+ });
119
100
  });
120
101
  });
121
102
 
122
103
  // Update users
123
104
  describe(`"PUT:${Paths.Users.Update}"`, () => {
124
105
 
125
- const DUMMY_USER = getDummyUsers()[0];
126
-
127
- // Setup API
128
- const callApi = (user: IUser | null, cb: TApiCb) =>
129
- agent
130
- .put(Paths.Users.Update)
131
- .send({ user })
132
- .end(wrapCb(cb));
133
-
134
106
  // Success
135
107
  it(`should return a status code of "${HttpStatusCodes.OK}" if the ` +
136
108
  'request was successful.', done => {
137
- spyOn(UserRepo, 'update').and.resolveTo();
138
- spyOn(UserRepo, 'persists').and.resolveTo(true);
139
- callApi(DUMMY_USER, res => {
140
- expect(res.status).toBe(HttpStatusCodes.OK);
141
- done();
142
- });
109
+ const user = DB_USERS[0];
110
+ user.name = 'Bill';
111
+ agent
112
+ .put(Paths.Users.Update)
113
+ .send({ user })
114
+ .end((_, res) => {
115
+ expect(res.status).toBe(HttpStatusCodes.OK);
116
+ done();
117
+ });
143
118
  });
144
119
 
145
120
  // Param missing
146
121
  it('should return a JSON object with an error message and a status code ' +
147
122
  `of "${HttpStatusCodes.BAD_REQUEST}" if the user param was missing`,
148
123
  done => {
149
- callApi(null, res => {
150
- expect(res.status).toBe(HttpStatusCodes.BAD_REQUEST);
151
- const errorObj = parseJson<IValidationErrFormat>(res.body.error);
152
- expect(errorObj.error).toBe(ValidationErr.MSG);
153
- expect(errorObj.parameter).toBe('user');
154
- done();
155
- });
124
+ agent
125
+ .put(Paths.Users.Update)
126
+ .send({ user: null })
127
+ .end((_, res) => {
128
+ expect(res.status).toBe(HttpStatusCodes.BAD_REQUEST);
129
+ const errorObj = parseJson<IValidationErr>(res.body.error);
130
+ expect(errorObj.message).toBe(ValidationErr.MSG);
131
+ expect(errorObj.parameters[0].prop).toBe('user');
132
+ done();
133
+ });
156
134
  });
157
135
 
158
136
  // User not found
159
137
  it('should return a JSON object with the error message of ' +
160
138
  `"${USER_NOT_FOUND_ERR}" and a status code of ` +
161
139
  `"${HttpStatusCodes.NOT_FOUND}" if the id was not found.`, done => {
162
- callApi(DUMMY_USER, res => {
163
- expect(res.status).toBe(HttpStatusCodes.NOT_FOUND);
164
- expect(res.body.error).toBe(USER_NOT_FOUND_ERR);
165
- done();
166
- });
140
+ const user = User.new({ id: 4, name: 'a', email: 'a@a.com' });
141
+ agent
142
+ .put(Paths.Users.Update)
143
+ .send({ user })
144
+ .end((_, res) => {
145
+ expect(res.status).toBe(HttpStatusCodes.NOT_FOUND);
146
+ expect(res.body.error).toBe(USER_NOT_FOUND_ERR);
147
+ done();
148
+ });
167
149
  });
168
150
  });
169
151
 
170
152
  // Delete User
171
153
  describe(`"DELETE:${Paths.Users.Delete}"`, () => {
172
154
 
173
- // Call API
174
- const callApi = (id: number, cb: TApiCb) =>
175
- agent
176
- .delete(insertUrlParams(Paths.Users.Delete, { id }))
177
- .end(wrapCb(cb));
155
+ const getPath = (id: number) => insertUrlParams(Paths.Users.Delete, { id });
178
156
 
179
157
  // Success
180
158
  it(`should return a status code of "${HttpStatusCodes.OK}" if the ` +
181
159
  'request was successful.', done => {
182
- spyOn(UserRepo, 'delete').and.resolveTo();
183
- spyOn(UserRepo, 'persists').and.resolveTo(true);
184
- callApi(5, res => {
185
- expect(res.status).toBe(HttpStatusCodes.OK);
186
- done();
187
- });
160
+ const id = dbUsers[0].id;
161
+ agent
162
+ .delete(getPath(id))
163
+ .end((_, res) => {
164
+ expect(res.status).toBe(HttpStatusCodes.OK);
165
+ done();
166
+ });
188
167
  });
189
168
 
190
169
  // User not found
191
170
  it('should return a JSON object with the error message of ' +
192
171
  `"${USER_NOT_FOUND_ERR}" and a status code of ` +
193
172
  `"${HttpStatusCodes.NOT_FOUND}" if the id was not found.`, done => {
194
- callApi(-1, res => {
195
- expect(res.status).toBe(HttpStatusCodes.NOT_FOUND);
196
- expect(res.body.error).toBe(USER_NOT_FOUND_ERR);
197
- done();
198
- });
173
+ agent
174
+ .delete(getPath(-1))
175
+ .end((_, res) => {
176
+ expect(res.status).toBe(HttpStatusCodes.NOT_FOUND);
177
+ expect(res.body.error).toBe(USER_NOT_FOUND_ERR);
178
+ done();
179
+ });
199
180
  });
200
181
  });
201
182
  });
@@ -1,18 +1,6 @@
1
1
  import HttpStatusCodes from '@src/common/HttpStatusCodes';
2
2
 
3
3
 
4
- /******************************************************************************
5
- Types
6
- ******************************************************************************/
7
-
8
- export interface IValidationErrFormat {
9
- error: string;
10
- parameter: string;
11
- value?: unknown;
12
- 'more-info'?: string;
13
- }
14
-
15
-
16
4
  /******************************************************************************
17
5
  Classes
18
6
  ******************************************************************************/
@@ -33,17 +21,13 @@ export class RouteError extends Error {
33
21
  * Validation in route layer errors.
34
22
  */
35
23
  export class ValidationErr extends RouteError {
36
- public static MSG = 'The following parameter was missing or invalid.';
37
-
38
- public constructor(parameter: string, value?: unknown, moreInfo?: string) {
39
- const msgObj: IValidationErrFormat = {
40
- error: ValidationErr.MSG,
41
- parameter,
42
- value,
43
- };
44
- if (!!moreInfo) {
45
- msgObj['more-info'] = moreInfo;
46
- }
47
- super(HttpStatusCodes.BAD_REQUEST, JSON.stringify(msgObj));
24
+ public static MSG = 'One or more parameters were missing or invalid.';
25
+
26
+ public constructor(errObj: unknown) {
27
+ const msg = JSON.stringify({
28
+ message: ValidationErr.MSG,
29
+ parameters: errObj,
30
+ });
31
+ super(HttpStatusCodes.BAD_REQUEST, msg);
48
32
  }
49
33
  }
@@ -1,6 +1,6 @@
1
1
  import logger from 'jet-logger';
2
2
 
3
- import Env from '@src/common/Env';
3
+ import ENV from '@src/common/ENV';
4
4
  import server from './server';
5
5
 
6
6
 
@@ -9,6 +9,6 @@ import server from './server';
9
9
  ******************************************************************************/
10
10
 
11
11
  const SERVER_START_MSG = ('Express server started on port: ' +
12
- Env.Port.toString());
12
+ ENV.Port.toString());
13
13
 
14
- server.listen(Env.Port, () => logger.info(SERVER_START_MSG));
14
+ server.listen(ENV.Port, () => logger.info(SERVER_START_MSG));
@@ -1,5 +1,7 @@
1
1
  import jsonfile from 'jsonfile';
2
2
 
3
+ import ENV from '@src/common/ENV';
4
+ import { NodeEnvs } from '@src/common/constants';
3
5
  import { IUser } from '@src/models/User';
4
6
 
5
7
 
@@ -7,7 +9,11 @@ import { IUser } from '@src/models/User';
7
9
  Variables
8
10
  ******************************************************************************/
9
11
 
10
- const DB_FILE_NAME = 'database.json';
12
+ const DB_FILE_NAME = (
13
+ ENV.NodeEnv === NodeEnvs.Test
14
+ ? 'database.test.json'
15
+ : 'database.json'
16
+ );
11
17
 
12
18
 
13
19
  /******************************************************************************
@@ -1,5 +1,6 @@
1
1
  import { IUser } from '@src/models/User';
2
2
  import { getRandomInt } from '@src/util/misc';
3
+
3
4
  import orm from './MockOrm';
4
5
 
5
6
 
@@ -83,6 +84,36 @@ async function delete_(id: number): Promise<void> {
83
84
  }
84
85
 
85
86
 
87
+ // **** Unit-Tests Only **** //
88
+
89
+ /**
90
+ * Delete every user record.
91
+ */
92
+ async function deleteAllUsers(): Promise<void> {
93
+ const db = await orm.openDb();
94
+ db.users = [];
95
+ return orm.saveDb(db);
96
+ }
97
+
98
+ /**
99
+ * Insert multiple users. Can't do multiple at once cause using a plain file
100
+ * for nmow.
101
+ */
102
+ async function insertMult(
103
+ users: IUser[] | readonly IUser[],
104
+ ): Promise<IUser[]> {
105
+ const db = await orm.openDb(),
106
+ usersF = [ ...users ];
107
+ for (const user of usersF) {
108
+ user.id = getRandomInt();
109
+ user.created = new Date();
110
+ }
111
+ db.users = [ ...db.users, ...users ];
112
+ await orm.saveDb(db);
113
+ return usersF;
114
+ }
115
+
116
+
86
117
  /******************************************************************************
87
118
  Export default
88
119
  ******************************************************************************/
@@ -94,4 +125,6 @@ export default {
94
125
  add,
95
126
  update,
96
127
  delete: delete_,
128
+ deleteAllUsers,
129
+ insertMult,
97
130
  } as const;
@@ -0,0 +1 @@
1
+ {"users":[{"id":697609403164,"name":"Bill","created":"2025-02-13T19:25:15.288Z","email":"sean.maxwell@gmail.com"},{"id":387041929717,"name":"John Smith","created":"2025-02-13T19:25:15.288Z","email":"john.smith@gmail.com"},{"id":465077000925,"name":"Gordan Freeman","created":"2025-02-13T19:25:15.288Z","email":"gordan.freeman@gmail.com"}]}
@@ -1,5 +1,5 @@
1
1
  import { Response, Request } from 'express';
2
- import { isObject } from 'jet-validators';
2
+ import { isObject, isString } from 'jet-validators';
3
3
  import { parseObject, TSchema } from 'jet-validators/utils';
4
4
 
5
5
  import { ValidationErr } from '@src/common/route-errors';
@@ -13,6 +13,12 @@ type TRecord = Record<string, unknown>;
13
13
  export type IReq = Request<TRecord, void, TRecord, TRecord>;
14
14
  export type IRes = Response<unknown, TRecord>;
15
15
 
16
+ export interface IReqPropErr {
17
+ prop: string;
18
+ value: unknown;
19
+ moreInfo?: string;
20
+ }
21
+
16
22
 
17
23
  /******************************************************************************
18
24
  Functions
@@ -22,27 +28,35 @@ export type IRes = Response<unknown, TRecord>;
22
28
  * Parse a Request object property and throw a Validation error if it fails.
23
29
  */
24
30
  export function parseReq<U extends TSchema>(schema: U) {
25
- const parseFn = parseObject<U>(schema, _parseReqOnError);
26
31
  return (arg: unknown) => {
32
+ // Don't alter original object
27
33
  if (isObject(arg)) {
28
34
  arg = { ...arg };
29
35
  }
30
- return parseFn(arg);
36
+ // Error callback
37
+ const errArr: IReqPropErr[] = [];
38
+ const errCb = (
39
+ prop = 'undefined',
40
+ value?: unknown,
41
+ caughtErr?: unknown,
42
+ ) => {
43
+ const err: IReqPropErr = { prop, value };
44
+ if (caughtErr !== undefined) {
45
+ let moreInfo;
46
+ if (!isString(caughtErr)) {
47
+ moreInfo = JSON.stringify(caughtErr);
48
+ } else {
49
+ moreInfo = caughtErr;
50
+ }
51
+ err.moreInfo = moreInfo;
52
+ }
53
+ errArr.push(err);
54
+ };
55
+ // Return
56
+ const retVal = parseObject<U>(schema, errCb)(arg);
57
+ if (errArr.length > 0) {
58
+ throw new ValidationErr(errArr);
59
+ }
60
+ return retVal;
31
61
  };
32
62
  }
33
-
34
- /**
35
- * Error handler for the request parse function above.
36
- */
37
- function _parseReqOnError(
38
- prop = 'undefined',
39
- value?: unknown,
40
- caughtErr?: unknown,
41
- ): void {
42
- if (caughtErr !== undefined) {
43
- const moreInfo = JSON.stringify(caughtErr);
44
- throw new ValidationErr(prop, value, moreInfo);
45
- } else {
46
- throw new ValidationErr(prop, value);
47
- }
48
- }
@@ -9,7 +9,7 @@ import 'express-async-errors';
9
9
  import BaseRouter from '@src/routes';
10
10
 
11
11
  import Paths from '@src/routes/common/Paths';
12
- import Env from '@src/common/Env';
12
+ import ENV from '@src/common/ENV';
13
13
  import HttpStatusCodes from '@src/common/HttpStatusCodes';
14
14
  import { RouteError } from '@src/common/route-errors';
15
15
  import { NodeEnvs } from '@src/common/constants';
@@ -29,12 +29,12 @@ app.use(express.json());
29
29
  app.use(express.urlencoded({extended: true}));
30
30
 
31
31
  // Show routes called in console during development
32
- if (Env.NodeEnv === NodeEnvs.Dev.valueOf()) {
32
+ if (ENV.NodeEnv === NodeEnvs.Dev.valueOf()) {
33
33
  app.use(morgan('dev'));
34
34
  }
35
35
 
36
36
  // Security
37
- if (Env.NodeEnv === NodeEnvs.Production.valueOf()) {
37
+ if (ENV.NodeEnv === NodeEnvs.Production.valueOf()) {
38
38
  app.use(helmet());
39
39
  }
40
40
 
@@ -43,7 +43,7 @@ app.use(Paths.Base, BaseRouter);
43
43
 
44
44
  // Add error handler
45
45
  app.use((err: Error, _: Request, res: Response, next: NextFunction) => {
46
- if (Env.NodeEnv !== NodeEnvs.Test.valueOf()) {
46
+ if (ENV.NodeEnv !== NodeEnvs.Test.valueOf()) {
47
47
  logger.err(err, true);
48
48
  }
49
49
  let status = HttpStatusCodes.BAD_REQUEST;
@@ -1,3 +1,4 @@
1
+
1
2
  /**
2
3
  * Get a random number between 1 and 1,000,000,000,000
3
4
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-generator-typescript",
3
- "version": "2.5.12",
3
+ "version": "2.5.15",
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
  "express-generator-typescript": "node bin/cli.js",
@@ -1,13 +0,0 @@
1
- import { Response } from 'supertest';
2
- import { IUser } from '@src/models/User';
3
-
4
-
5
- // Misc
6
- export type TRes = Omit<Response, 'body'> & {
7
- body: {
8
- error?: string,
9
- user?: IUser,
10
- users?: IUser[],
11
- },
12
- };
13
- export type TApiCb = (res: TRes) => void;
@@ -1,14 +0,0 @@
1
- import { IUser } from '@src/models/User';
2
- import 'supertest';
3
-
4
-
5
- declare module 'supertest' {
6
-
7
- export interface Response {
8
- headers: Record<string, string[]>;
9
- body: {
10
- error: string,
11
- users: IUser[],
12
- };
13
- }
14
- }
File without changes