@mshima/generator-jhipster-nodejs 0.0.0-stage → 0.0.1

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.
Files changed (209) hide show
  1. package/LICENSE.txt +204 -0
  2. package/README.md +243 -2
  3. package/build-info.json +3 -0
  4. package/cli/cli-customizations.cjs +9 -0
  5. package/cli/cli.cjs +40 -0
  6. package/cli/logo.js +31 -0
  7. package/cli/version.cjs +12 -0
  8. package/generators/ci-cd/USAGE +5 -0
  9. package/generators/ci-cd/command.js +5 -0
  10. package/generators/ci-cd/generator.js +158 -0
  11. package/generators/ci-cd/index.js +2 -0
  12. package/generators/ci-cd/templates/.gitlab-ci.yml.ejs +137 -0
  13. package/generators/ci-cd/templates/azure-pipelines.yml.ejs +68 -0
  14. package/generators/ci-cd/templates/circle.yml.ejs +59 -0
  15. package/generators/ci-cd/templates/docker-registry.yml.ejs +58 -0
  16. package/generators/ci-cd/templates/github-ci.yml.ejs +90 -0
  17. package/generators/ci-cd/templates/jenkins/Jenkinsfile.ejs +104 -0
  18. package/generators/ci-cd/templates/jenkins/idea.gdsl +90 -0
  19. package/generators/ci-cd/templates/jenkins/jenkins.yml.ejs +33 -0
  20. package/generators/ci-cd/templates/travis.yml.ejs +75 -0
  21. package/generators/client/command.js +5 -0
  22. package/generators/client/generator.js +21 -0
  23. package/generators/client/generators/bootstrap/command.js +5 -0
  24. package/generators/client/generators/bootstrap/generator.js +28 -0
  25. package/generators/client/generators/bootstrap/index.js +2 -0
  26. package/generators/client/index.js +2 -0
  27. package/generators/common/generators/bootstrap/command.js +5 -0
  28. package/generators/common/generators/bootstrap/generator.js +37 -0
  29. package/generators/common/generators/bootstrap/index.js +2 -0
  30. package/generators/docker/command.js +5 -0
  31. package/generators/docker/generator.js +16 -0
  32. package/generators/docker/generators/bootstrap/command.js +5 -0
  33. package/generators/docker/generators/bootstrap/generator.js +56 -0
  34. package/generators/docker/generators/bootstrap/index.js +2 -0
  35. package/generators/docker/index.js +2 -0
  36. package/generators/docker/templates/docker/app.yml.ejs +25 -0
  37. package/generators/generator-nodejs-constants.js +32 -0
  38. package/generators/languages/command.js +5 -0
  39. package/generators/languages/generator.js +22 -0
  40. package/generators/languages/index.js +2 -0
  41. package/generators/languages/templates/src/main/webapp/i18n/en/home.json +19 -0
  42. package/generators/languages/templates/src/main/webapp/i18n/es/home.json +19 -0
  43. package/generators/languages/templates/src/main/webapp/i18n/it/home.json +19 -0
  44. package/generators/node-server/command.js +96 -0
  45. package/generators/node-server/entity-files.js +41 -0
  46. package/generators/node-server/files.js +254 -0
  47. package/generators/node-server/generator.js +702 -0
  48. package/generators/node-server/generators/bootstrap/command.js +5 -0
  49. package/generators/node-server/generators/bootstrap/generator.js +83 -0
  50. package/generators/node-server/generators/bootstrap/index.js +2 -0
  51. package/generators/node-server/index.js +2 -0
  52. package/generators/node-server/resources/package.json +76 -0
  53. package/generators/node-server/support/api-fake-data.js +54 -0
  54. package/generators/node-server/support/columns.js +73 -0
  55. package/generators/node-server/support/database.js +24 -0
  56. package/generators/node-server/support/entity-authorities.js +32 -0
  57. package/generators/node-server/support/entity-paths.js +13 -0
  58. package/generators/node-server/support/enums.js +11 -0
  59. package/generators/node-server/support/index.js +9 -0
  60. package/generators/node-server/support/mail-templates.js +82 -0
  61. package/generators/node-server/support/migrations.js +197 -0
  62. package/generators/node-server/support/validators.js +25 -0
  63. package/generators/node-server/templates/.dockerignore.ejs +16 -0
  64. package/generators/node-server/templates/.gitignore.jhi.nodejs.ejs +24 -0
  65. package/generators/node-server/templates/Dockerfile.ejs +54 -0
  66. package/generators/node-server/templates/README.md.jhi.nodejs.ejs +220 -0
  67. package/generators/node-server/templates/package.json.ejs +84 -0
  68. package/generators/node-server/templates/server/README.md.ejs +269 -0
  69. package/generators/node-server/templates/server/e2e/_entityFileName_.database.e2e-spec.ts.ejs +192 -0
  70. package/generators/node-server/templates/server/e2e/_entityFileName_.e2e-spec.ts.ejs +402 -0
  71. package/generators/node-server/templates/server/e2e/account.e2e-spec.ts.ejs +349 -0
  72. package/generators/node-server/templates/server/e2e/app.e2e-spec.ts.ejs +188 -0
  73. package/generators/node-server/templates/server/e2e/authority.e2e-spec.ts.ejs +50 -0
  74. package/generators/node-server/templates/server/e2e/cors.e2e-spec.ts.ejs +110 -0
  75. package/generators/node-server/templates/server/e2e/management.e2e-spec.ts.ejs +180 -0
  76. package/generators/node-server/templates/server/e2e/oauth2-bearer.e2e-spec.ts.ejs +71 -0
  77. package/generators/node-server/templates/server/e2e/session.e2e-spec.ts.ejs +219 -0
  78. package/generators/node-server/templates/server/e2e/setup.test.js.ejs +1 -0
  79. package/generators/node-server/templates/server/e2e/user.e2e-spec.ts.ejs +318 -0
  80. package/generators/node-server/templates/server/env.ejs +1 -0
  81. package/generators/node-server/templates/server/eslint.config.mjs.ejs +48 -0
  82. package/generators/node-server/templates/server/nest-cli.json.ejs +11 -0
  83. package/generators/node-server/templates/server/ormconfig.ts.ejs +35 -0
  84. package/generators/node-server/templates/server/package.json.ejs +143 -0
  85. package/generators/node-server/templates/server/sonar-project.properties.ejs +10 -0
  86. package/generators/node-server/templates/server/src/app.config.spec.ts.ejs +13 -0
  87. package/generators/node-server/templates/server/src/app.config.ts.ejs +179 -0
  88. package/generators/node-server/templates/server/src/app.module.ts.ejs +80 -0
  89. package/generators/node-server/templates/server/src/application-config.ts.ejs +153 -0
  90. package/generators/node-server/templates/server/src/auth/account.controller.ts.ejs +183 -0
  91. package/generators/node-server/templates/server/src/auth/auth.module.ts.ejs +104 -0
  92. package/generators/node-server/templates/server/src/auth/auth.service.ts.ejs +294 -0
  93. package/generators/node-server/templates/server/src/auth/csrf.middleware.spec.ts.ejs +65 -0
  94. package/generators/node-server/templates/server/src/auth/csrf.middleware.ts.ejs +76 -0
  95. package/generators/node-server/templates/server/src/auth/decorators/auth-user.decorator.ts.ejs +8 -0
  96. package/generators/node-server/templates/server/src/auth/decorators/roles.decorator.ts.ejs +4 -0
  97. package/generators/node-server/templates/server/src/auth/dto/key-and-password.dto.ts.ejs +17 -0
  98. package/generators/node-server/templates/server/src/auth/dto/password-change.dto.ts.ejs +17 -0
  99. package/generators/node-server/templates/server/src/auth/dto/user-login.dto.ts.ejs +21 -0
  100. package/generators/node-server/templates/server/src/auth/guards/auth.guard.ts.ejs +50 -0
  101. package/generators/node-server/templates/server/src/auth/guards/oauth2-login.guard.ts.ejs +6 -0
  102. package/generators/node-server/templates/server/src/auth/guards/roles.guard.ts.ejs +25 -0
  103. package/generators/node-server/templates/server/src/auth/index.ts.ejs +8 -0
  104. package/generators/node-server/templates/server/src/auth/jwt.config.spec.ts.ejs +22 -0
  105. package/generators/node-server/templates/server/src/auth/jwt.config.ts.ejs +29 -0
  106. package/generators/node-server/templates/server/src/auth/oauth2.authorities.spec.ts.ejs +24 -0
  107. package/generators/node-server/templates/server/src/auth/oauth2.authorities.ts.ejs +33 -0
  108. package/generators/node-server/templates/server/src/auth/oauth2.config.ts.ejs +51 -0
  109. package/generators/node-server/templates/server/src/auth/passport.jwt.strategy.ts.ejs +45 -0
  110. package/generators/node-server/templates/server/src/auth/passport.oauth2-bearer.strategy.ts.ejs +32 -0
  111. package/generators/node-server/templates/server/src/auth/passport.oauth2.strategy.spec.ts.ejs +27 -0
  112. package/generators/node-server/templates/server/src/auth/passport.oauth2.strategy.ts.ejs +44 -0
  113. package/generators/node-server/templates/server/src/auth/password-util.ts.ejs +10 -0
  114. package/generators/node-server/templates/server/src/auth/password.service.ts.ejs +34 -0
  115. package/generators/node-server/templates/server/src/auth/payload.interface.ts.ejs +14 -0
  116. package/generators/node-server/templates/server/src/auth/role-type.ts.ejs +7 -0
  117. package/generators/node-server/templates/server/src/auth/session-user.middleware.ts.ejs +33 -0
  118. package/generators/node-server/templates/server/src/auth/session.entity.ts.ejs +36 -0
  119. package/generators/node-server/templates/server/src/auth/session.store.spec.ts.ejs +99 -0
  120. package/generators/node-server/templates/server/src/auth/session.store.ts.ejs +116 -0
  121. package/generators/node-server/templates/server/src/auth/user.jwt.controller.ts.ejs +26 -0
  122. package/generators/node-server/templates/server/src/auth/user.oauth2.controller.ts.ejs +78 -0
  123. package/generators/node-server/templates/server/src/auth/user.session.controller.ts.ejs +103 -0
  124. package/generators/node-server/templates/server/src/common/common.module.ts.ejs +19 -0
  125. package/generators/node-server/templates/server/src/common/database/base.entity.ts.ejs +13 -0
  126. package/generators/node-server/templates/server/src/common/database/column-transformers.spec.ts.ejs +46 -0
  127. package/generators/node-server/templates/server/src/common/database/column-transformers.ts.ejs +69 -0
  128. package/generators/node-server/templates/server/src/common/database/column-types.spec.ts.ejs +21 -0
  129. package/generators/node-server/templates/server/src/common/database/column-types.ts.ejs +29 -0
  130. package/generators/node-server/templates/server/src/common/database/pagination.entity.spec.ts.ejs +66 -0
  131. package/generators/node-server/templates/server/src/common/database/pagination.entity.ts.ejs +108 -0
  132. package/generators/node-server/templates/server/src/common/dto/base.dto.ts.ejs +19 -0
  133. package/generators/node-server/templates/server/src/common/errors/error-constants.ts.ejs +10 -0
  134. package/generators/node-server/templates/server/src/common/errors/problem-details.filter.ts.ejs +63 -0
  135. package/generators/node-server/templates/server/src/common/errors/problem.exception.ts.ejs +92 -0
  136. package/generators/node-server/templates/server/src/common/field_validators.ejs +77 -0
  137. package/generators/node-server/templates/server/src/common/header-util.ts.ejs +105 -0
  138. package/generators/node-server/templates/server/src/common/interceptors/logging.interceptor.spec.ts.ejs +93 -0
  139. package/generators/node-server/templates/server/src/common/interceptors/logging.interceptor.ts.ejs +48 -0
  140. package/generators/node-server/templates/server/src/common/logging.spec.ts.ejs +83 -0
  141. package/generators/node-server/templates/server/src/common/logging.ts.ejs +129 -0
  142. package/generators/node-server/templates/server/src/common/metrics.spec.ts.ejs +43 -0
  143. package/generators/node-server/templates/server/src/common/metrics.ts.ejs +198 -0
  144. package/generators/node-server/templates/server/src/common/request.ts.ejs +8 -0
  145. package/generators/node-server/templates/server/src/common/sortable-properties.ts.ejs +43 -0
  146. package/generators/node-server/templates/server/src/common/updatable-properties.ts.ejs +14 -0
  147. package/generators/node-server/templates/server/src/config/application-dev.yml.ejs +118 -0
  148. package/generators/node-server/templates/server/src/config/application-prod.yml.ejs +97 -0
  149. package/generators/node-server/templates/server/src/config/application-secret-samples.yml.ejs +42 -0
  150. package/generators/node-server/templates/server/src/config/application-test.yml.ejs +68 -0
  151. package/generators/node-server/templates/server/src/config/application-tls-samples.yml.ejs +15 -0
  152. package/generators/node-server/templates/server/src/config/application-tls.yml.ejs +15 -0
  153. package/generators/node-server/templates/server/src/config/application.yml.ejs +149 -0
  154. package/generators/node-server/templates/server/src/config.spec.ts.ejs +81 -0
  155. package/generators/node-server/templates/server/src/config.ts.ejs +255 -0
  156. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.controller.ts.ejs +178 -0
  157. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.entity.ts.ejs +110 -0
  158. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.mapper.ts.ejs +37 -0
  159. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.module.ts.ejs +14 -0
  160. package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.service.ts.ejs +123 -0
  161. package/generators/node-server/templates/server/src/entities/_entityFileName_/dto/_entityFileName_.dto.ts.ejs +99 -0
  162. package/generators/node-server/templates/server/src/mail/mail.module.ts.ejs +49 -0
  163. package/generators/node-server/templates/server/src/mail/mail.service.spec.ts.ejs +105 -0
  164. package/generators/node-server/templates/server/src/mail/mail.service.ts.ejs +120 -0
  165. package/generators/node-server/templates/server/src/main.ts.ejs +114 -0
  166. package/generators/node-server/templates/server/src/management/api-docs.controller.ts.ejs +27 -0
  167. package/generators/node-server/templates/server/src/management/api-docs.service.ts.ejs +8 -0
  168. package/generators/node-server/templates/server/src/management/configuration.controller.ts.ejs +80 -0
  169. package/generators/node-server/templates/server/src/management/health.controller.spec.ts.ejs +79 -0
  170. package/generators/node-server/templates/server/src/management/health.controller.ts.ejs +87 -0
  171. package/generators/node-server/templates/server/src/management/health.module.ts.ejs +9 -0
  172. package/generators/node-server/templates/server/src/management/loggers.controller.ts.ejs +40 -0
  173. package/generators/node-server/templates/server/src/management/management.controller.spec.ts.ejs +26 -0
  174. package/generators/node-server/templates/server/src/management/management.controller.ts.ejs +34 -0
  175. package/generators/node-server/templates/server/src/management/management.module.ts.ejs +21 -0
  176. package/generators/node-server/templates/server/src/management/metrics.controller.ts.ejs +85 -0
  177. package/generators/node-server/templates/server/src/migrations/1570200270081-CreateTables.ts.ejs +64 -0
  178. package/generators/node-server/templates/server/src/migrations/1570200270082-CreateSessions.ts.ejs +26 -0
  179. package/generators/node-server/templates/server/src/migrations/1570200490072-SeedUsersRoles.ts.ejs +74 -0
  180. package/generators/node-server/templates/server/src/migrations/_entityConstraintsMigration_.ts.ejs +40 -0
  181. package/generators/node-server/templates/server/src/migrations/_entityMigration_.ts.ejs +26 -0
  182. package/generators/node-server/templates/server/src/migrations/support/schema.ts.ejs +202 -0
  183. package/generators/node-server/templates/server/src/orm.config.ts.ejs +147 -0
  184. package/generators/node-server/templates/server/src/registry/consul.service.ts.ejs +70 -0
  185. package/generators/node-server/templates/server/src/registry/eureka.service.ts.ejs +129 -0
  186. package/generators/node-server/templates/server/src/swagger.ts.ejs +24 -0
  187. package/generators/node-server/templates/server/src/tls.ts.ejs +27 -0
  188. package/generators/node-server/templates/server/src/users/authority.controller.ts.ejs +85 -0
  189. package/generators/node-server/templates/server/src/users/authority.entity.ts.ejs +22 -0
  190. package/generators/node-server/templates/server/src/users/dto/authority.dto.ts.ejs +10 -0
  191. package/generators/node-server/templates/server/src/users/dto/user.dto.ts.ejs +116 -0
  192. package/generators/node-server/templates/server/src/users/public.user.controller.ts.ejs +51 -0
  193. package/generators/node-server/templates/server/src/users/user.controller.ts.ejs +191 -0
  194. package/generators/node-server/templates/server/src/users/user.entity.ts.ejs +63 -0
  195. package/generators/node-server/templates/server/src/users/user.mapper.ts.ejs +26 -0
  196. package/generators/node-server/templates/server/src/users/user.module.ts.ejs +19 -0
  197. package/generators/node-server/templates/server/src/users/user.service.ts.ejs +156 -0
  198. package/generators/node-server/templates/server/tsconfig.build.json.ejs +9 -0
  199. package/generators/node-server/templates/server/tsconfig.json.ejs +23 -0
  200. package/generators/node-server/templates/server/typeorm-migration.ts.ejs +18 -0
  201. package/generators/node-server/templates/server/vitest.config.e2e.ts.ejs +16 -0
  202. package/generators/node-server/templates/server/vitest.config.ts.ejs +16 -0
  203. package/generators/server/command.js +7 -0
  204. package/generators/server/generator.js +20 -0
  205. package/generators/server/generators/bootstrap/command.js +5 -0
  206. package/generators/server/generators/bootstrap/generator.js +15 -0
  207. package/generators/server/generators/bootstrap/index.js +2 -0
  208. package/generators/server/index.js +2 -0
  209. package/package.json +75 -4
@@ -0,0 +1,108 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ import { Type, Expose as JsonProperty } from 'class-transformer';
3
+ import type { ParsedQs } from 'qs';
4
+
5
+ export type SortDirection = 'ASC' | 'DESC';
6
+
7
+ export class Sort {
8
+ public property = 'id';
9
+ public direction: SortDirection = 'ASC';
10
+ constructor(sort?: string) {
11
+ if (sort) {
12
+ const [property, direction] = sort.split(',');
13
+ this.property = property.trim();
14
+ // Like Spring Data, the direction is case insensitive and ascending by default.
15
+ this.direction = direction?.trim().toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
16
+ }
17
+ }
18
+
19
+ /** The TypeORM order of the property, nested for a property of a relationship like `region.name`. */
20
+ asOrder(): any {
21
+ return this.property
22
+ .split('.')
23
+ .reverse()
24
+ .reduce<any>((order, key) => ({ [key]: order }), this.direction);
25
+ }
26
+ }
27
+
28
+ export type PaginationQueryType = string | ParsedQs | (string | ParsedQs)[];
29
+
30
+ export class PageRequest {
31
+ /** The maximum size of a page, the default of Spring Data. */
32
+ static readonly MAX_SIZE = 2000;
33
+
34
+ @JsonProperty()
35
+ page = 0;
36
+ @JsonProperty()
37
+ size = 20;
38
+ @Type(() => Sort)
39
+ sort: Sort[] = [new Sort('id,ASC')];
40
+
41
+ constructor(
42
+ page?: number | PaginationQueryType,
43
+ size?: number | PaginationQueryType
44
+ ,sort?: PaginationQueryType,
45
+ /**
46
+ * The properties a request can sort on: a list, like the allowed ordered properties of JHipster's UserResource, or a
47
+ * check of a property, any when not given.
48
+ */
49
+ sortProperties?: string[] | ((property: string) => boolean)
50
+ ) {
51
+ this.page = Math.max(PageRequest.handleNumberTypes(page, this.page), 0);
52
+ const requestedSize = PageRequest.handleNumberTypes(size, this.size);
53
+ this.size = requestedSize < 1 ? this.size : Math.min(requestedSize, PageRequest.MAX_SIZE);
54
+ const sorts = PageRequest.handleQueryTypes(sort).map(value => new Sort(value));
55
+ const isSortable = typeof sortProperties === 'function' ? sortProperties : (property: string): boolean => sortProperties?.includes(property) ?? true;
56
+ const invalid = sorts.find(each => !isSortable(each.property));
57
+ if (invalid) {
58
+ throw new BadRequestException(`Invalid sort property: ${invalid.property}`);
59
+ }
60
+ if (sorts.length > 0) {
61
+ this.sort = sorts;
62
+ }
63
+ }
64
+
65
+ /** The TypeORM order of every sort, in the order of the request, the properties of a relationship merged. */
66
+ asOrder(): any {
67
+ const order: any = {};
68
+ for (const { property, direction } of this.sort) {
69
+ const keys = property.split('.');
70
+ let target = order;
71
+ for (const key of keys.slice(0, -1)) {
72
+ target = target[key] ??= {};
73
+ }
74
+ target[keys[keys.length - 1]] = direction;
75
+ }
76
+ return order;
77
+ }
78
+
79
+ /** Every string of a query parameter given several times, like several `sort` parameters. */
80
+ public static handleQueryTypes(pa: PaginationQueryType | undefined): string[] {
81
+ return (Array.isArray(pa) ? pa : [pa]).filter((value): value is string => typeof value === 'string' && value.length > 0);
82
+ }
83
+
84
+ public static handleNumberTypes(pp: number | PaginationQueryType | undefined, fallback: number): number {
85
+ if (typeof pp === 'number') {
86
+ return pp;
87
+ }
88
+ const query = PageRequest.handleQueryType(pp);
89
+ if (query) {
90
+ const parsed = parseInt(query, 10);
91
+ return Number.isNaN(parsed) ? fallback : parsed;
92
+ }
93
+ return fallback;
94
+ }
95
+
96
+ public static handleQueryType(pa: PaginationQueryType | undefined): string | undefined {
97
+ pa = Array.isArray(pa) ? pa[0] : pa;
98
+ if (typeof pa === 'string') {
99
+ return pa;
100
+ }
101
+ return undefined;
102
+ }
103
+ }
104
+
105
+ /** A page of a list: its records (entities, or the users of the public user list), the total and the page request. */
106
+ export class Page<T> {
107
+ constructor(public content: T[], public total: number, public pageable: PageRequest) {}
108
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A DTO base object.
3
+ */
4
+ export class BaseDTO {
5
+ createdBy?: string;
6
+
7
+ createdDate?: Date;
8
+
9
+ lastModifiedBy?: string;
10
+
11
+ lastModifiedDate?: Date;
12
+ }
13
+
14
+ /**
15
+ * The type of a relationship of a DTO, like the Relation of the TypeORM entities: two related DTOs import each other, and
16
+ * ES modules evaluate such a cycle one module at a time, so the decorator metadata of the property must not reference
17
+ * the other class (with this type it is Object), or the first module fails with "Cannot access ... before initialization".
18
+ */
19
+ export type RelatedDTO<T> = T;
@@ -0,0 +1,10 @@
1
+ /** The problem types and the translation keys of the errors, like the ErrorConstants of JHipster Spring Boot. */
2
+ export const ERR_VALIDATION = 'error.validation';
3
+ export const PROBLEM_BASE_URL = 'https://www.jhipster.tech/problem';
4
+ export const DEFAULT_TYPE = `${PROBLEM_BASE_URL}/problem-with-message`;
5
+ export const CONSTRAINT_VIOLATION_TYPE = `${PROBLEM_BASE_URL}/constraint-violation`;
6
+ <%_ if (generateUserManagement) { _%>
7
+ export const INVALID_PASSWORD_TYPE = `${PROBLEM_BASE_URL}/invalid-password`;
8
+ export const EMAIL_ALREADY_USED_TYPE = `${PROBLEM_BASE_URL}/email-already-used`;
9
+ export const LOGIN_ALREADY_USED_TYPE = `${PROBLEM_BASE_URL}/login-already-used`;
10
+ <%_ } _%>
@@ -0,0 +1,63 @@
1
+ import { STATUS_CODES } from 'http';
2
+
3
+ import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';
4
+ import { Request, Response } from 'express';
5
+
6
+ import { HeaderUtil } from '../header-util.ts';
7
+
8
+ import { DEFAULT_TYPE } from './error-constants.ts';
9
+ import { BadRequestAlertException, ProblemException } from './problem.exception.ts';
10
+
11
+ /**
12
+ * Answers the errors as the problem details (RFC 9457, application/problem+json) of JHipster Spring Boot, which the
13
+ * JHipster clients read: the type of the problem, the translation key of the error (message), the path of the request
14
+ * and the errors of the fields. An unexpected error is logged, and answered without its message.
15
+ */
16
+ @Catch()
17
+ export class ProblemDetailsFilter implements ExceptionFilter {
18
+ private readonly logger = new Logger('ProblemDetailsFilter');
19
+
20
+ constructor(private readonly headerUtil: HeaderUtil) {}
21
+
22
+ catch(exception: unknown, host: ArgumentsHost): void {
23
+ const http = host.switchToHttp();
24
+ const request = http.getRequest<Request>();
25
+ const response = http.getResponse<Response>();
26
+ const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
27
+ if (!(exception instanceof HttpException)) {
28
+ this.logger.error(`${request.method} ${request.originalUrl}`, exception instanceof Error ? exception.stack : String(exception));
29
+ }
30
+ if (response.headersSent) {
31
+ return;
32
+ }
33
+ if (exception instanceof BadRequestAlertException) {
34
+ // Like the ExceptionTranslator of JHipster Spring Boot, the alert headers the JHipster clients read first.
35
+ this.headerUtil.addFailureAlertHeaders(response, exception.entityName, exception.errorKey);
36
+ }
37
+ response
38
+ .status(status)
39
+ .type('application/problem+json')
40
+ .json({
41
+ type: DEFAULT_TYPE,
42
+ title: STATUS_CODES[status],
43
+ status,
44
+ detail: exception instanceof HttpException ? detailOf(exception) : undefined,
45
+ message: `error.http.${status}`,
46
+ path: request.path,
47
+ ...(exception instanceof ProblemException ? exception.problem : {}),
48
+ });
49
+ }
50
+ }
51
+
52
+ /** The message of an HttpException of Nest: the string it was created with, or the message of its response body. */
53
+ const detailOf = (exception: HttpException): string | undefined => {
54
+ if (exception instanceof ProblemException) {
55
+ return exception.problem.detail;
56
+ }
57
+ const body = exception.getResponse();
58
+ if (typeof body === 'string') {
59
+ return body;
60
+ }
61
+ const { message } = body as { message?: string | string[] };
62
+ return Array.isArray(message) ? message.join(', ') : message;
63
+ };
@@ -0,0 +1,92 @@
1
+ import { HttpException, HttpStatus, ValidationError } from '@nestjs/common';
2
+
3
+ import { CONSTRAINT_VIOLATION_TYPE, DEFAULT_TYPE, ERR_VALIDATION<% if (generateUserManagement) { %>, EMAIL_ALREADY_USED_TYPE, INVALID_PASSWORD_TYPE, LOGIN_ALREADY_USED_TYPE<% } %> } from './error-constants.ts';
4
+
5
+ /** An error of a field, like the FieldErrorVM of JHipster Spring Boot. */
6
+ export interface FieldErrorVM {
7
+ objectName: string;
8
+ field: string;
9
+ message: string;
10
+ }
11
+
12
+ /** The properties of a problem detail the exception gives, the others are completed by the ProblemDetailsFilter. */
13
+ export interface ProblemProperties {
14
+ type?: string;
15
+ title?: string;
16
+ detail?: string;
17
+ /** The translation key of the error. */
18
+ message?: string;
19
+ /** The parameter of the translation, the entity name. */
20
+ params?: string;
21
+ fieldErrors?: FieldErrorVM[];
22
+ }
23
+
24
+ /** An exception answered as the problem detail it gives. */
25
+ export class ProblemException extends HttpException {
26
+ constructor(
27
+ status: number,
28
+ readonly problem: ProblemProperties,
29
+ ) {
30
+ super(problem, status);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * The errors of the validation of a request body, like the field errors of JHipster Spring Boot: the object (the class
36
+ * of the body without its <%= dtoSuffix %> suffix), the field and the message of the first constraint it fails.
37
+ */
38
+ export const toFieldErrors = (errors: ValidationError[], parentPath = ''): FieldErrorVM[] =>
39
+ errors.flatMap(error => {
40
+ const field = `${parentPath}${error.property}`;
41
+ const objectName = (error.target?.constructor.name ?? '').replace(/^./, first => first.toLowerCase()).replace(/<%= dtoSuffix %>$/, '');
42
+ return [
43
+ ...Object.values(error.constraints ?? {})
44
+ .slice(0, 1)
45
+ .map(message => ({ objectName, field, message })),
46
+ ...toFieldErrors(error.children ?? [], `${field}.`),
47
+ ];
48
+ });
49
+
50
+ /** The validation of a request body failed, like the MethodArgumentNotValidException of Spring. */
51
+ export class ValidationException extends ProblemException {
52
+ constructor(errors: ValidationError[]) {
53
+ super(HttpStatus.BAD_REQUEST, {
54
+ type: CONSTRAINT_VIOLATION_TYPE,
55
+ title: 'Method argument not valid',
56
+ message: ERR_VALIDATION,
57
+ fieldErrors: toFieldErrors(errors),
58
+ });
59
+ }
60
+ }
61
+
62
+ /** A bad request of an entity, like the BadRequestAlertException of JHipster Spring Boot. */
63
+ export class BadRequestAlertException extends ProblemException {
64
+ constructor(
65
+ defaultMessage: string,
66
+ readonly entityName: string,
67
+ readonly errorKey: string,
68
+ type = DEFAULT_TYPE,
69
+ ) {
70
+ super(HttpStatus.BAD_REQUEST, { type, title: defaultMessage, message: `error.${errorKey}`, params: entityName });
71
+ }
72
+ }
73
+ <%_ if (generateUserManagement) { _%>
74
+
75
+ export class LoginAlreadyUsedException extends BadRequestAlertException {
76
+ constructor() {
77
+ super('Login name already used!', 'userManagement', 'userexists', LOGIN_ALREADY_USED_TYPE);
78
+ }
79
+ }
80
+
81
+ export class EmailAlreadyUsedException extends BadRequestAlertException {
82
+ constructor() {
83
+ super('Email is already in use!', 'userManagement', 'emailexists', EMAIL_ALREADY_USED_TYPE);
84
+ }
85
+ }
86
+
87
+ export class InvalidPasswordException extends ProblemException {
88
+ constructor() {
89
+ super(HttpStatus.BAD_REQUEST, { type: INVALID_PASSWORD_TYPE, title: 'Incorrect password' });
90
+ }
91
+ }
92
+ <%_ } _%>
@@ -0,0 +1,77 @@
1
+ <%#
2
+ Copyright 2019-2023 the original author or authors from the JHipster project.
3
+
4
+ This file is part of the JHipster project, see https://www.jhipster.tech/
5
+ for more information.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+ -%>
19
+ <%# Returns a string of all validator annotations for the entity field. -%>
20
+ <%
21
+
22
+ let result = '';
23
+ const validators = [];
24
+
25
+ if (field.fieldValidate === true) {
26
+ const rules = field.fieldValidateRules;
27
+ const MAX_VALUE = 2147483647;
28
+ // A required blob too: without a validator, the whitelist of the ValidationPipe strips it from the request.
29
+ if (rules.includes('required')) {
30
+ validators.push('@IsNotEmpty()');
31
+ } else if (!rules.includes('required')) {
32
+ // Like Bean Validation, the other rules accept a missing (null or undefined) value.
33
+ validators.push('@IsOptional()');
34
+ }
35
+ if (rules.includes('minlength') && !rules.includes('maxlength')) {
36
+ validators.push('@MinLength(' + field.fieldValidateRulesMinlength + ')');
37
+ }
38
+ if (rules.includes('maxlength') && !rules.includes('minlength')) {
39
+ validators.push('@MaxLength(' + field.fieldValidateRulesMaxlength + ')');
40
+ }
41
+ if (rules.includes('minlength') && rules.includes('maxlength')) {
42
+ validators.push('@Length(' + field.fieldValidateRulesMinlength + ', ' + field.fieldValidateRulesMaxlength + ')');
43
+ }
44
+ // Not supported anymore because the server can't check the size of the blob before downloading it completely.
45
+ // if (rules.includes('minbytes') && !rules.includes('maxbytes')) {
46
+ // validators.push('@Size(min = ' + field.fieldValidateRulesMinbytes + ')');
47
+ // }
48
+ // if (rules.includes('maxbytes') && !rules.includes('minbytes')) {
49
+ // validators.push('@Size(max = ' + field.fieldValidateRulesMaxbytes + ')');
50
+ // }
51
+ // if (rules.includes('minbytes') && rules.includes('maxbytes')) {
52
+ // validators.push('@Size(min = ' + field.fieldValidateRulesMinbytes + ', max = ' + field.fieldValidateRulesMaxbytes + ')');
53
+ // }
54
+ if (rules.includes('min')) {
55
+ validators.push('@Min(' + field.fieldValidateRulesMin + ')');
56
+ }
57
+ if (rules.includes('max')) {
58
+ validators.push('@Max(' + field.fieldValidateRulesMax + ')');
59
+ }
60
+ if (rules.includes('pattern')) {
61
+ // Like Bean Validation @Pattern, the whole value matches the pattern.
62
+ validators.push('@Matches(' + JSON.stringify('^(?:' + field.fieldValidateRulesPattern + ')$') + ')');
63
+ }
64
+ } else if (field.fieldIsEnum || field.nodejsTypeValidators.length > 0) {
65
+ validators.push('@IsOptional()');
66
+ }
67
+ // Like the JSON deserialization of JHipster Spring Boot, a value of another type is a bad request (400).
68
+ validators.push(...field.nodejsTypeValidators);
69
+ if (field.fieldIsEnum) {
70
+ // Like the JSON deserialization of JHipster Spring Boot, a value out of the enum is a bad request (400), not a database error.
71
+ validators.push('@IsEnum(' + field.fieldType + ')');
72
+ }
73
+ if (validators.length > 0) {
74
+ result = validators.join('\n ');
75
+ result += '\n';
76
+ } -%>
77
+ <%- result -%>
@@ -0,0 +1,105 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { Response } from 'express';
4
+ import { URL } from 'url';
5
+
6
+ import type { ApplicationConfig } from '../application-config.ts';
7
+ import { Page } from './database/pagination.entity.ts';
8
+
9
+ /**
10
+ * The headers of the responses the JHipster clients read, like the HeaderUtil and the PaginationUtil of JHipster Spring
11
+ * Boot: the alerts, named after the application (jhipster.clientApp.name), and the pagination.
12
+ */
13
+ @Injectable()
14
+ export class HeaderUtil {
15
+ private readonly applicationName: string;
16
+
17
+ constructor(configService: ConfigService<ApplicationConfig, true>) {
18
+ this.applicationName = configService.get('jhipster.clientApp.name', { infer: true });
19
+ }
20
+
21
+ createAlert(res: Response, message: string, param: string): any {
22
+ res.set(`X-${this.applicationName}-alert`, message);
23
+ res.set(`X-${this.applicationName}-params`, param);
24
+ }
25
+
26
+ addEntityCreatedHeaders(res: Response, entityName: string, param: string | number | undefined): any {
27
+ res.status(201);
28
+ <%_ if (enableTranslation) { _%>
29
+ // The translation key of the alert, translated by the client.
30
+ this.createAlert(res, `${this.applicationName}.${entityName}.created`, String(param));
31
+ <%_ } else { _%>
32
+ this.createAlert(res, `A new ${entityName} is created with identifier ${param}`, String(param));
33
+ <%_ } _%>
34
+ }
35
+
36
+ addEntityUpdatedHeaders(res: Response, entityName: string, param: string | number | undefined): any {
37
+ res.status(200);
38
+ <%_ if (enableTranslation) { _%>
39
+ // The translation key of the alert, translated by the client.
40
+ this.createAlert(res, `${this.applicationName}.${entityName}.updated`, String(param));
41
+ <%_ } else { _%>
42
+ this.createAlert(res, `A ${entityName} is updated with identifier ${param}`, String(param));
43
+ <%_ } _%>
44
+ }
45
+
46
+ addEntityDeletedHeaders(res: Response, entityName: string, param: string | number | undefined): any {
47
+ res.status(204);
48
+ <%_ if (enableTranslation) { _%>
49
+ // The translation key of the alert, translated by the client.
50
+ this.createAlert(res, `${this.applicationName}.${entityName}.deleted`, String(param));
51
+ <%_ } else { _%>
52
+ this.createAlert(res, `A ${entityName} is deleted with identifier ${param}`, String(param));
53
+ <%_ } _%>
54
+ }
55
+
56
+ /**
57
+ * The alerts of the user management, like the UserResource of JHipster Spring Boot: translated by the client from its
58
+ * userManagement keys.
59
+ */
60
+ addUserManagementHeaders(res: Response, action: 'created' | 'updated' | 'deleted', login: string): any {
61
+ res.status({ created: 201, updated: 200, deleted: 204 }[action]);
62
+ <%_ if (enableTranslation) { _%>
63
+ this.createAlert(res, `userManagement.${action}`, login);
64
+ <%_ } else { _%>
65
+ this.createAlert(res, `A user is ${action} with identifier ${login}`, login);
66
+ <%_ } _%>
67
+ }
68
+
69
+ /**
70
+ * Like the failure alert of JHipster Spring Boot (BadRequestAlertException), the status comes from the thrown exception.
71
+ */
72
+ addFailureAlertHeaders(res: Response, entityName: string, errorKey: string): any {
73
+ res.set(`X-${this.applicationName}-error`, `error.${errorKey}`);
74
+ res.set(`X-${this.applicationName}-params`, entityName);
75
+ }
76
+
77
+ /**
78
+ * The pagination headers, like the PaginationUtil of JHipster Spring Boot: the total count of the elements, and the
79
+ * links of the next, previous, last and first pages.
80
+ */
81
+ addPaginationHeaders<T>(res: Response, page: Page<T>): any {
82
+ const url = res.req.url;
83
+ res.set('X-Total-Count', page.total.toString());
84
+ const pageNumber = page.pageable.page;
85
+ const pageSize = page.pageable.size;
86
+ const totalPages = Math.ceil(page.total / pageSize);
87
+ const links: string[] = [];
88
+ if (pageNumber < totalPages - 1) {
89
+ links.push(this.prepareLink(url, pageNumber + 1, pageSize, 'next'));
90
+ }
91
+ if (pageNumber > 0) {
92
+ links.push(this.prepareLink(url, pageNumber - 1, pageSize, 'prev'));
93
+ }
94
+ links.push(this.prepareLink(url, Math.max(totalPages - 1, 0), pageSize, 'last'));
95
+ links.push(this.prepareLink(url, 0, pageSize, 'first'));
96
+ res.set('Link', links.join(','));
97
+ }
98
+
99
+ private prepareLink(url: string, pageNumber: number, pageSize: number, relType: string): string {
100
+ const link = new URL(`http://localhost${url}`);
101
+ link.searchParams.set('page', String(pageNumber));
102
+ link.searchParams.set('size', String(pageSize));
103
+ return `<${link.toString().replace('http://localhost', '')}>; rel="${relType}"`;
104
+ }
105
+ }
@@ -0,0 +1,93 @@
1
+ import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest';
2
+ import { CallHandler, Controller, ExecutionContext, Get, Logger } from '@nestjs/common';
3
+ import { Reflector } from '@nestjs/core';
4
+ import { EventEmitter } from 'events';
5
+ import { of } from 'rxjs';
6
+ import { LoggingInterceptor, SkipLogging } from './logging.interceptor.ts';
7
+
8
+ @Controller('test')
9
+ class TestController {
10
+ @Get('/logged')
11
+ logged(): string {
12
+ return 'logged';
13
+ }
14
+
15
+ @Get('/skipped')
16
+ @SkipLogging()
17
+ skipped(): string {
18
+ return 'skipped';
19
+ }
20
+ }
21
+
22
+ @Controller('skipped')
23
+ @SkipLogging()
24
+ class SkippedController {
25
+ @Get('/')
26
+ get(): string {
27
+ return 'skipped';
28
+ }
29
+ }
30
+
31
+ describe('LoggingInterceptor', () => {
32
+ const interceptor = new LoggingInterceptor(new Reflector());
33
+ const next: CallHandler = { handle: () => of('result') };
34
+ let response: EventEmitter & { statusCode: number };
35
+ let debug: MockInstance<Logger['debug']>;
36
+ let error: MockInstance<Logger['error']>;
37
+
38
+ const contextOf = (controller: any, handler: string): ExecutionContext =>
39
+ ({
40
+ getType: () => 'http',
41
+ getClass: () => controller,
42
+ getHandler: () => controller.prototype[handler],
43
+ switchToHttp: () => ({
44
+ getRequest: () => ({ method: 'GET', originalUrl: '/test/logged' }),
45
+ getResponse: () => response,
46
+ }),
47
+ }) as unknown as ExecutionContext;
48
+
49
+ beforeEach(() => {
50
+ response = Object.assign(new EventEmitter(), { statusCode: 200 });
51
+ debug = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined);
52
+ error = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined);
53
+ });
54
+
55
+ afterEach(() => {
56
+ vi.restoreAllMocks();
57
+ });
58
+
59
+ it('logs the method, URL, status and elapsed time once the response is sent', () => {
60
+ interceptor.intercept(contextOf(TestController, 'logged'), next);
61
+ expect(debug).not.toHaveBeenCalled();
62
+ response.emit('finish');
63
+ expect(debug).toHaveBeenCalledWith(expect.stringMatching(/^GET \/test\/logged 200 \d+ms - TestController\.logged\(\)$/));
64
+ expect(error).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('logs the client errors at the debug level', () => {
68
+ interceptor.intercept(contextOf(TestController, 'logged'), next);
69
+ response.statusCode = 404;
70
+ response.emit('finish');
71
+ expect(debug).toHaveBeenCalledWith(expect.stringMatching(/^GET \/test\/logged 404 \d+ms/));
72
+ });
73
+
74
+ it('logs the server errors at the error level', () => {
75
+ interceptor.intercept(contextOf(TestController, 'logged'), next);
76
+ response.statusCode = 500;
77
+ response.emit('finish');
78
+ expect(error).toHaveBeenCalledWith(expect.stringMatching(/^GET \/test\/logged 500 \d+ms/));
79
+ expect(debug).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it('does not log a handler with @SkipLogging()', () => {
83
+ interceptor.intercept(contextOf(TestController, 'skipped'), next);
84
+ response.emit('finish');
85
+ expect(debug).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it('does not log a controller with @SkipLogging()', () => {
89
+ interceptor.intercept(contextOf(SkippedController, 'get'), next);
90
+ response.emit('finish');
91
+ expect(debug).not.toHaveBeenCalled();
92
+ });
93
+ });
@@ -0,0 +1,48 @@
1
+ import { CallHandler, CustomDecorator, ExecutionContext, Injectable, Logger, NestInterceptor, SetMetadata } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { Observable } from 'rxjs';
4
+ import { Request, Response } from 'express';
5
+
6
+ export const SKIP_LOGGING = 'skipLogging';
7
+
8
+ /**
9
+ * Excludes a controller, or one of its handlers, from the request logging of the LoggingInterceptor, like the health
10
+ * endpoint that the container and the service discovery health checks call every few seconds.
11
+ */
12
+ export const SkipLogging = (): CustomDecorator<string> => SetMetadata(SKIP_LOGGING, true);
13
+
14
+ /**
15
+ * Logs every request handled by a controller once its response is sent: method, URL, status and elapsed time.
16
+ * Registered globally in the AppModule (APP_INTERCEPTOR), opt out with @SkipLogging().
17
+ * Like the LoggingAspect of JHipster Spring Boot, the requests are logged at the debug level, the server errors (5xx) at the error level.
18
+ */
19
+ @Injectable()
20
+ export class LoggingInterceptor implements NestInterceptor {
21
+ private readonly logger = new Logger('LoggingInterceptor');
22
+
23
+ constructor(private readonly reflector: Reflector) {}
24
+
25
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
26
+ if (
27
+ context.getType() !== 'http' ||
28
+ this.reflector.getAllAndOverride<boolean>(SKIP_LOGGING, [context.getHandler(), context.getClass()])
29
+ ) {
30
+ return next.handle();
31
+ }
32
+ const request = context.switchToHttp().getRequest<Request>();
33
+ const response = context.switchToHttp().getResponse<Response>();
34
+ const handler = `${context.getClass().name}.${context.getHandler().name}()`;
35
+ const start = Date.now();
36
+ // Logged once the response is sent: the status of an exception is only set by the exception filters, after the
37
+ // interceptors, and the elapsed time includes the serialization of the response.
38
+ response.once('finish', () => {
39
+ const message = `${request.method} ${request.originalUrl} ${response.statusCode} ${Date.now() - start}ms - ${handler}`;
40
+ if (response.statusCode >= 500) {
41
+ this.logger.error(message);
42
+ } else {
43
+ this.logger.debug(message);
44
+ }
45
+ });
46
+ return next.handle();
47
+ }
48
+ }