express-generator-typescript 2.4.3 → 2.4.6

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.
@@ -20,7 +20,7 @@ const PROJECT_FOLDER_PATH = './project-files';
20
20
  // Project-folder dependencies
21
21
  const DEPENDENCIES = 'express dotenv morgan cookie-parser jet-logger ' +
22
22
  'module-alias express-async-errors helmet jsonfile inserturlparams ' +
23
- 'jet-validator ts-command-line-args',
23
+ 'jet-validator ts-command-line-args jet-paths',
24
24
  DEPENDENCIES_AUTH = ' bcrypt jsonwebtoken',
25
25
  DEV_DEPENDENCIES = 'ts-node typescript nodemon find jasmine supertest ' +
26
26
  '@types/node @types/express @types/jasmine @types/find @types/morgan ' +
@@ -36,6 +36,7 @@
36
36
  "inserturlparams": "^1.0.1",
37
37
  "jet-logger": "^1.3.1",
38
38
  "jet-validator": "^1.0.3",
39
+ "jet-paths": "^1.0.5",
39
40
  "jsonfile": "^6.1.0",
40
41
  "jsonwebtoken": "^8.5.1",
41
42
  "module-alias": "^2.2.2",
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Convert paths to full for testing.
3
+ */
4
+
5
+ import jetPaths from 'jet-paths';
6
+
7
+ import Paths from '@src/constants/Paths';
8
+
9
+
10
+ export default jetPaths(Paths);
@@ -8,21 +8,13 @@ import UserRepo from '@src/repos/UserRepo';
8
8
  import User from '@src/models/User';
9
9
  import HttpStatusCodes from '@src/constants/HttpStatusCodes';
10
10
  import { USER_NOT_FOUND_ERR } from '@src/services/UserService';
11
- import FullPaths from '@src/routes/constants/FullPaths';
12
11
 
12
+ import Paths from 'spec/support/Paths';
13
13
  import { TReqBody } from 'spec/support/types';
14
14
 
15
15
 
16
16
  // **** Variables **** //
17
17
 
18
- // Paths
19
- const {
20
- Get,
21
- Add,
22
- Update,
23
- Delete,
24
- } = FullPaths.Users;
25
-
26
18
  // StatusCodes
27
19
  const {
28
20
  OK,
@@ -57,9 +49,9 @@ describe('UserRouter', () => {
57
49
  });
58
50
 
59
51
  // ** Get all users ** //
60
- describe(`"GET:${Get}"`, () => {
52
+ describe(`"GET:${Paths.Users.Get}"`, () => {
61
53
 
62
- const callApi = () => agent.get(Get);
54
+ const callApi = () => agent.get(Paths.Users.Get);
63
55
 
64
56
  // Success
65
57
  it('should return a JSON object with all the users and a status code ' +
@@ -80,13 +72,13 @@ describe('UserRouter', () => {
80
72
  });
81
73
 
82
74
  // Test add user
83
- describe(`"POST:${Add}"`, () => {
75
+ describe(`"POST:${Paths.Users.Add}"`, () => {
84
76
 
85
77
  const ERROR_MSG = `${ValidatorErr}"user".`;
86
78
 
87
79
  const callApi = (reqBody: TReqBody) =>
88
80
  agent
89
- .post(Add)
81
+ .post(Paths.Users.Add)
90
82
  .type('form').send(reqBody);
91
83
 
92
84
  // Test add user success
@@ -118,13 +110,13 @@ describe('UserRouter', () => {
118
110
  });
119
111
 
120
112
  // ** Update users ** //
121
- describe(`"PUT:${Update}"`, () => {
113
+ describe(`"PUT:${Paths.Users.Update}"`, () => {
122
114
 
123
115
  const ERROR_MSG = `${ValidatorErr}"user".`;
124
116
 
125
117
  const callApi = (reqBody: TReqBody) =>
126
118
  agent
127
- .put(Update)
119
+ .put(Paths.Users.Update)
128
120
  .type('form').send(reqBody);
129
121
 
130
122
  // Success
@@ -170,13 +162,13 @@ describe('UserRouter', () => {
170
162
  });
171
163
 
172
164
  // ** Delete user ** //
173
- describe(`"DELETE:${Delete}"`, () => {
165
+ describe(`"DELETE:${Paths.Users.Delete}"`, () => {
174
166
 
175
167
  const VALIDATOR_ERR = `${ValidatorErr}"id".`;
176
168
 
177
169
  const callApi = (id: number) =>
178
170
  agent
179
- .delete(insertUrlParams(Delete, { id }));
171
+ .delete(insertUrlParams(Paths.Users.Delete, { id }));
180
172
 
181
173
  // Success
182
174
  it(`should return a status code of "${OK}" if the request was successful.`,
@@ -2,10 +2,8 @@
2
2
  * Express router paths go here.
3
3
  */
4
4
 
5
- import { Immutable } from '@src/other/types';
6
5
 
7
-
8
- const Paths = {
6
+ export default {
9
7
  Base: '/api',
10
8
  Users: {
11
9
  Base: '/users',
@@ -14,10 +12,4 @@ const Paths = {
14
12
  Update: '/update',
15
13
  Delete: '/delete/:id',
16
14
  },
17
- };
18
-
19
-
20
- // **** Export **** //
21
-
22
- export type TPaths = Immutable<typeof Paths>;
23
- export default Paths as TPaths;
15
+ } as const;
@@ -1 +1 @@
1
- {"users":[{"name":"Sean Maxwell","email":"sean.maxwell@gmail.com","pwdHash":"$2b$12$1mE2OI9hMS/rgH9Mi0s85OM2V5gzm7aF3gJIWH1y0S1MqVBueyjsy","role":1,"id":159123164363},{"name":"Gordan Freeman","email":"gordan.freeman@halflife.com","pwdHash":"$2b$12$1mE2OI9hMS/rgH9Mi0s85OM2V5gzm7aF3gJIWH1y0S1MqVBueyjsy","role":0,"id":906524522143},{"name":"John Smith","email":"jsmith@yahoo.com","pwdHash":"$2b$12$1mE2OI9hMS/rgH9Mi0s85OM2V5gzm7aF3gJIWH1y0S1MqVBueyjsy","role":0,"id":357437875835},{"id":75800032258,"name":"asdf","email":"asdfasdf","role":0}]}
1
+ {"users":[{"name":"Sean Maxwell","email":"sean.maxwell@gmail.com","pwdHash":"$2b$12$1mE2OI9hMS/rgH9Mi0s85OM2V5gzm7aF3gJIWH1y0S1MqVBueyjsy","role":1,"id":159123164363},{"name":"Gordan Freeman","email":"gordan.freeman@halflife.com","pwdHash":"$2b$12$1mE2OI9hMS/rgH9Mi0s85OM2V5gzm7aF3gJIWH1y0S1MqVBueyjsy","role":0,"id":906524522143},{"name":"John Smith","email":"jsmith@yahoo.com","pwdHash":"$2b$12$1mE2OI9hMS/rgH9Mi0s85OM2V5gzm7aF3gJIWH1y0S1MqVBueyjsy","role":0,"id":357437875835}]}
@@ -1,7 +1,7 @@
1
1
  import { Router } from 'express';
2
2
  import jetValidator from 'jet-validator';
3
3
 
4
- import Paths from './constants/Paths';
4
+ import Paths from '../constants/Paths';
5
5
  import User from '@src/models/User';
6
6
  import UserRoutes from './UserRoutes';
7
7
 
@@ -12,7 +12,7 @@ import logger from 'jet-logger';
12
12
  import 'express-async-errors';
13
13
 
14
14
  import BaseRouter from '@src/routes/api';
15
- import Paths from '@src/routes/constants/Paths';
15
+ import Paths from '@src/constants/Paths';
16
16
 
17
17
  import EnvVars from '@src/constants/EnvVars';
18
18
  import HttpStatusCodes from '@src/constants/HttpStatusCodes';
@@ -36,6 +36,7 @@
36
36
  "inserturlparams": "^1.0.1",
37
37
  "jet-logger": "^1.3.1",
38
38
  "jet-validator": "^1.0.3",
39
+ "jet-paths": "^1.0.5",
39
40
  "jsonfile": "^6.1.0",
40
41
  "jsonwebtoken": "^8.5.1",
41
42
  "module-alias": "^2.2.2",
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Convert paths to full for testing.
3
+ */
4
+
5
+ import jetPaths from 'jet-paths';
6
+
7
+ import Paths from '@src/constants/Paths';
8
+
9
+
10
+ export default jetPaths(Paths);
@@ -3,7 +3,8 @@ import { SuperTest, Test, Response } from 'supertest';
3
3
  import User, { UserRoles } from '@src/models/User';
4
4
  import UserRepo from '@src/repos/UserRepo';
5
5
  import PwdUtil from '@src/util/PwdUtil';
6
- import FullPaths from '@src/routes/constants/FullPaths';
6
+
7
+ import Paths from './Paths';
7
8
 
8
9
 
9
10
  // **** Variables **** //
@@ -28,7 +29,7 @@ function login(beforeAgent: SuperTest<Test>, done: (arg: string) => void) {
28
29
  spyOn(UserRepo, 'getOne').and.resolveTo(loginUser);
29
30
  // Call Login API
30
31
  beforeAgent
31
- .post(FullPaths.Auth.Login)
32
+ .post(Paths.Auth.Login)
32
33
  .type('form')
33
34
  .send(LoginCreds)
34
35
  .end((_: Error, res: Response) => {
@@ -7,21 +7,15 @@ import PwdUtil from '@src/util/PwdUtil';
7
7
  import User, { UserRoles } from '@src/models/User';
8
8
  import { Errors } from '@src/services/AuthService';
9
9
 
10
- import FullPaths from '@src/routes/constants/FullPaths';
11
10
  import EnvVars from '@src/constants/EnvVars';
12
11
  import HttpStatusCodes from '@src/constants/HttpStatusCodes';
13
12
 
13
+ import Paths from 'spec/support/Paths';
14
14
  import { TReqBody } from 'spec/support/types';
15
15
 
16
16
 
17
17
  // **** Variables **** //
18
18
 
19
- // Paths
20
- const {
21
- Login,
22
- Logout,
23
- } = FullPaths.Auth;
24
-
25
19
  // StatusCodes
26
20
  const {
27
21
  OK,
@@ -48,13 +42,13 @@ describe('AuthRouter', () => {
48
42
  });
49
43
 
50
44
  // ** Test login ** //
51
- describe(`"POST:${Login}"`, () => {
45
+ describe(`"POST:${Paths.Auth.Login}"`, () => {
52
46
 
53
47
  const EMAIL_NOT_FOUND_ERR = Errors.EmailNotFound(LoginCreds.email);
54
48
 
55
49
  const callApi = (reqBody: TReqBody) =>
56
50
  agent
57
- .post(Login)
51
+ .post(Paths.Auth.Login)
58
52
  .type('form')
59
53
  .send(reqBody);
60
54
 
@@ -114,11 +108,11 @@ describe('AuthRouter', () => {
114
108
  });
115
109
 
116
110
  // ** Test logout ** //
117
- describe(`"GET:${Logout}"`, () => {
111
+ describe(`"GET:${Paths.Auth.Logout}"`, () => {
118
112
 
119
113
  // Successful logout
120
114
  it(`should return a response with a status of ${OK}`, (done) => {
121
- agent.get(Logout)
115
+ agent.get(Paths.Auth.Logout)
122
116
  .end((_: Error, res: Response) => {
123
117
  expect(res.status).toBe(HttpStatusCodes.OK);
124
118
  done();
@@ -8,22 +8,14 @@ import UserRepo from '@src/repos/UserRepo';
8
8
  import User from '@src/models/User';
9
9
  import HttpStatusCodes from '@src/constants/HttpStatusCodes';
10
10
  import { USER_NOT_FOUND_ERR } from '@src/services/UserService';
11
- import FullPaths from '@src/routes/constants/FullPaths';
12
11
 
13
12
  import login from '../support/login';
13
+ import Paths from 'spec/support/Paths';
14
14
  import { TReqBody } from 'spec/support/types';
15
15
 
16
16
 
17
17
  // **** Variables **** //
18
18
 
19
- // Paths
20
- const {
21
- Get,
22
- Add,
23
- Update,
24
- Delete,
25
- } = FullPaths.Users;
26
-
27
19
  // StatusCodes
28
20
  const {
29
21
  OK,
@@ -62,11 +54,11 @@ describe('UserRouter', () => {
62
54
  });
63
55
 
64
56
  // ** Get all users ** //
65
- describe(`"GET:${Get}"`, () => {
57
+ describe(`"GET:${Paths.Users.Get}"`, () => {
66
58
 
67
59
  const callApi = () =>
68
60
  agent
69
- .get(Get)
61
+ .get(Paths.Users.Get)
70
62
  .set('Cookie', jwtCookie);
71
63
 
72
64
  // Success
@@ -88,13 +80,13 @@ describe('UserRouter', () => {
88
80
  });
89
81
 
90
82
  // Test add user
91
- describe(`"POST:${Add}"`, () => {
83
+ describe(`"POST:${Paths.Users.Add}"`, () => {
92
84
 
93
85
  const ERROR_MSG = `${ValidatorErr}"user".`;
94
86
 
95
87
  const callApi = (reqBody: TReqBody) =>
96
88
  agent
97
- .post(Add)
89
+ .post(Paths.Users.Add)
98
90
  .set('Cookie', jwtCookie)
99
91
  .type('form').send(reqBody);
100
92
 
@@ -127,13 +119,13 @@ describe('UserRouter', () => {
127
119
  });
128
120
 
129
121
  // ** Update users ** //
130
- describe(`"PUT:${Update}"`, () => {
122
+ describe(`"PUT:${Paths.Users.Update}"`, () => {
131
123
 
132
124
  const ERROR_MSG = `${ValidatorErr}"user".`;
133
125
 
134
126
  const callApi = (reqBody: TReqBody) =>
135
127
  agent
136
- .put(Update)
128
+ .put(Paths.Users.Update)
137
129
  .set('Cookie', jwtCookie)
138
130
  .type('form').send(reqBody);
139
131
 
@@ -180,13 +172,13 @@ describe('UserRouter', () => {
180
172
  });
181
173
 
182
174
  // ** Delete user ** //
183
- describe(`"DELETE:${Delete}"`, () => {
175
+ describe(`"DELETE:${Paths.Users.Delete}"`, () => {
184
176
 
185
177
  const VALIDATOR_ERR = `${ValidatorErr}"id".`;
186
178
 
187
179
  const callApi = (id: number) =>
188
180
  agent
189
- .delete(insertUrlParams(Delete, { id }))
181
+ .delete(insertUrlParams(Paths.Users.Delete, { id }))
190
182
  .set('Cookie', jwtCookie);
191
183
 
192
184
  // Success
@@ -2,10 +2,8 @@
2
2
  * Express router paths go here.
3
3
  */
4
4
 
5
- import { Immutable } from '@src/other/types';
6
5
 
7
-
8
- const Paths = {
6
+ export default {
9
7
  Base: '/api',
10
8
  Auth: {
11
9
  Base: '/auth',
@@ -19,10 +17,4 @@ const Paths = {
19
17
  Update: '/update',
20
18
  Delete: '/delete/:id',
21
19
  },
22
- };
23
-
24
-
25
- // **** Export **** //
26
-
27
- export type TPaths = Immutable<typeof Paths>;
28
- export default Paths as TPaths;
20
+ } as const;
@@ -2,7 +2,7 @@ import { Router } from 'express';
2
2
  import jetValidator from 'jet-validator';
3
3
 
4
4
  import adminMw from './middleware/adminMw';
5
- import Paths from './constants/Paths';
5
+ import Paths from '../constants/Paths';
6
6
  import User from '@src/models/User';
7
7
  import AuthRoutes from './AuthRoutes';
8
8
  import UserRoutes from './UserRoutes';
@@ -12,7 +12,7 @@ import logger from 'jet-logger';
12
12
  import 'express-async-errors';
13
13
 
14
14
  import BaseRouter from '@src/routes/api';
15
- import Paths from '@src/routes/constants/Paths';
15
+ import Paths from '@src/constants/Paths';
16
16
 
17
17
  import EnvVars from '@src/constants/EnvVars';
18
18
  import HttpStatusCodes from '@src/constants/HttpStatusCodes';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-generator-typescript",
3
- "version": "2.4.3",
3
+ "version": "2.4.6",
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,4 +0,0 @@
1
-
2
- export type Immutable<T> = {
3
- readonly [K in keyof T]: Immutable<T[K]>;
4
- };
@@ -1,40 +0,0 @@
1
-
2
- /**
3
- * Convert paths to full paths.
4
- */
5
-
6
- import Paths, { TPaths } from './Paths';
7
-
8
-
9
- interface IPathObj {
10
- Base: string;
11
- [key: string]: string | IPathObj;
12
- }
13
-
14
- /**
15
- * The recursive function.
16
- */
17
- function getFullPaths(
18
- parent: IPathObj,
19
- baseUrl: string,
20
- ): IPathObj {
21
- const url = (baseUrl + parent.Base),
22
- keys = Object.keys(parent),
23
- retVal: IPathObj = { Base: url };
24
- // Iterate keys
25
- for (const key of keys) {
26
- const pval = parent[key];
27
- if (key !== 'Base' && typeof pval === 'string') {
28
- retVal[key] = (url + pval);
29
- } else if (typeof pval === 'object') {
30
- retVal[key] = getFullPaths(pval, url);
31
- }
32
- }
33
- // Return
34
- return retVal;
35
- }
36
-
37
-
38
- // **** Export default **** //
39
-
40
- export default getFullPaths(Paths, '') as TPaths;
@@ -1,56 +0,0 @@
1
- import UserRepo from '@src/repos/UserRepo';
2
-
3
- import PwdUtil from '@src/util/PwdUtil';
4
- import { tick } from '@src/util/misc';
5
-
6
- import HttpStatusCodes from '@src/constants/HttpStatusCodes';
7
- import { RouteError } from '@src/other/classes';
8
- import { IUser } from '@src/models/User';
9
-
10
-
11
- // **** Variables **** //
12
-
13
- // Errors
14
- export const Errors = {
15
- Unauth: 'Unauthorized',
16
- EmailNotFound(email: string) {
17
- return `User with email "${email}" not found`;
18
- },
19
- } as const;
20
-
21
-
22
- // **** Functions **** //
23
-
24
- /**
25
- * Login a user.
26
- */
27
- async function login(email: string, password: string): Promise<IUser> {
28
- // Fetch user
29
- const user = await UserRepo.getOne(email);
30
- if (!user) {
31
- throw new RouteError(
32
- HttpStatusCodes.UNAUTHORIZED,
33
- Errors.EmailNotFound(email),
34
- );
35
- }
36
- // Check password
37
- const hash = (user.pwdHash ?? ''),
38
- pwdPassed = await PwdUtil.compare(password, hash);
39
- if (!pwdPassed) {
40
- // If password failed, wait 500ms this will increase security
41
- await tick(500);
42
- throw new RouteError(
43
- HttpStatusCodes.UNAUTHORIZED,
44
- Errors.Unauth,
45
- );
46
- }
47
- // Return
48
- return user;
49
- }
50
-
51
-
52
- // **** Export default **** //
53
-
54
- export default {
55
- login,
56
- } as const;
@@ -1,39 +0,0 @@
1
- import bcrypt from 'bcrypt';
2
-
3
-
4
- // **** Variables **** //
5
-
6
- const SALT_ROUNDS = 12;
7
-
8
-
9
- // **** Functions **** //
10
-
11
- /**
12
- * Get a hash from the password.
13
- */
14
- function getHash(pwd: string): Promise<string> {
15
- return bcrypt.hash(pwd, SALT_ROUNDS);
16
- }
17
-
18
- /**
19
- * Useful for testing.
20
- */
21
- function hashSync(pwd: string): string {
22
- return bcrypt.hashSync(pwd, SALT_ROUNDS);
23
- }
24
-
25
- /**
26
- * See if a password passes the hash.
27
- */
28
- function compare(pwd: string, hash: string): Promise<boolean> {
29
- return bcrypt.compare(pwd, hash);
30
- }
31
-
32
-
33
- // **** Export Default **** //
34
-
35
- export default {
36
- getHash,
37
- hashSync,
38
- compare,
39
- } as const;
@@ -1,92 +0,0 @@
1
- import { Request, Response } from 'express';
2
-
3
- import HttpStatusCodes from '@src/constants/HttpStatusCodes';
4
- import { RouteError } from '@src/other/classes';
5
- import jsonwebtoken from 'jsonwebtoken';
6
-
7
- import EnvVars from '../constants/EnvVars';
8
-
9
-
10
- // **** Variables **** //
11
-
12
- // Errors
13
- const Errors = {
14
- ParamFalsey: 'Param is falsey',
15
- Validation: 'JSON-web-token validation failed.',
16
- } as const;
17
-
18
- // Options
19
- const Options = {
20
- expiresIn: EnvVars.Jwt.Exp,
21
- };
22
-
23
-
24
- // **** Functions **** //
25
-
26
- /**
27
- * Get session data from request object (i.e. ISessionUser)
28
- */
29
- function getSessionData<T>(req: Request): Promise<string | T | undefined> {
30
- const { Key } = EnvVars.CookieProps,
31
- jwt = req.signedCookies[Key];
32
- return _decode(jwt);
33
- }
34
-
35
- /**
36
- * Add a JWT to the response
37
- */
38
- async function addSessionData(
39
- res: Response,
40
- data: string | object,
41
- ): Promise<Response> {
42
- if (!res || !data) {
43
- throw new RouteError(HttpStatusCodes.BAD_REQUEST, Errors.ParamFalsey);
44
- }
45
- // Setup JWT
46
- const jwt = await _sign(data),
47
- { Key, Options } = EnvVars.CookieProps;
48
- // Return
49
- return res.cookie(Key, jwt, Options);
50
- }
51
-
52
- /**
53
- * Remove cookie
54
- */
55
- function clearCookie(res: Response): Response {
56
- const { Key, Options } = EnvVars.CookieProps;
57
- return res.clearCookie(Key, Options);
58
- }
59
-
60
-
61
- // **** Helper Functions **** //
62
-
63
- /**
64
- * Encrypt data and return jwt.
65
- */
66
- function _sign(data: string | object | Buffer): Promise<string> {
67
- return new Promise((res, rej) => {
68
- jsonwebtoken.sign(data, EnvVars.Jwt.Secret, Options, (err, token) => {
69
- return err ? rej(err) : res(token || '');
70
- });
71
- });
72
- }
73
-
74
- /**
75
- * Decrypt JWT and extract client data.
76
- */
77
- function _decode<T>(jwt: string): Promise<string | undefined | T> {
78
- return new Promise((res, rej) => {
79
- jsonwebtoken.verify(jwt, EnvVars.Jwt.Secret, (err, decoded) => {
80
- return err ? rej(Errors.Validation) : res(decoded as T);
81
- });
82
- });
83
- }
84
-
85
-
86
- // **** Export default **** //
87
-
88
- export default {
89
- addSessionData,
90
- getSessionData,
91
- clearCookie,
92
- } as const;
@@ -1,4 +0,0 @@
1
-
2
- export type Immutable<T> = {
3
- readonly [K in keyof T]: Immutable<T[K]>;
4
- };
@@ -1,40 +0,0 @@
1
-
2
- /**
3
- * Convert paths to full paths.
4
- */
5
-
6
- import Paths, { TPaths } from './Paths';
7
-
8
-
9
- interface IPathObj {
10
- Base: string;
11
- [key: string]: string | IPathObj;
12
- }
13
-
14
- /**
15
- * The recursive function.
16
- */
17
- function getFullPaths(
18
- parent: IPathObj,
19
- baseUrl: string,
20
- ): IPathObj {
21
- const url = (baseUrl + parent.Base),
22
- keys = Object.keys(parent),
23
- retVal: IPathObj = { Base: url };
24
- // Iterate keys
25
- for (const key of keys) {
26
- const pval = parent[key];
27
- if (key !== 'Base' && typeof pval === 'string') {
28
- retVal[key] = (url + pval);
29
- } else if (typeof pval === 'object') {
30
- retVal[key] = getFullPaths(pval, url);
31
- }
32
- }
33
- // Return
34
- return retVal;
35
- }
36
-
37
-
38
- // **** Export default **** //
39
-
40
- export default getFullPaths(Paths, '') as TPaths;