create-easy-stack 1.0.3

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 (327) hide show
  1. package/bin/create.mjs +244 -0
  2. package/infra/email/files/src/core/config/email.env.ts +73 -0
  3. package/infra/email/files/src/core/infrastructure/email/email-dispatcher.port.ts +13 -0
  4. package/infra/email/files/src/core/infrastructure/email/email-layout.spec.ts +83 -0
  5. package/infra/email/files/src/core/infrastructure/email/email-layout.ts +88 -0
  6. package/infra/email/files/src/core/infrastructure/email/email-sender.factory.spec.ts +87 -0
  7. package/infra/email/files/src/core/infrastructure/email/email-sender.factory.ts +35 -0
  8. package/infra/email/files/src/core/infrastructure/email/email-sender.port.ts +19 -0
  9. package/infra/email/files/src/core/infrastructure/email/email.job.spec.ts +101 -0
  10. package/infra/email/files/src/core/infrastructure/email/email.job.ts +64 -0
  11. package/infra/email/files/src/core/infrastructure/email/email.module.spec.ts +60 -0
  12. package/infra/email/files/src/core/infrastructure/email/email.module.ts +34 -0
  13. package/infra/email/files/src/core/infrastructure/email/log-email-sender.spec.ts +34 -0
  14. package/infra/email/files/src/core/infrastructure/email/log-email-sender.ts +17 -0
  15. package/infra/email/files/src/core/infrastructure/email/smtp-email-sender.spec.ts +99 -0
  16. package/infra/email/files/src/core/infrastructure/email/smtp-email-sender.ts +63 -0
  17. package/infra/email/infra.json +58 -0
  18. package/infra/prisma-postgres/docker.service.yml +12 -0
  19. package/infra/prisma-postgres/files/prisma/schema.prisma +10 -0
  20. package/infra/prisma-postgres/files/prisma.config.mjs +10 -0
  21. package/infra/prisma-postgres/files/src/core/config/database.env.ts +13 -0
  22. package/infra/prisma-postgres/files/src/core/infrastructure/database/database.module.ts +9 -0
  23. package/infra/prisma-postgres/files/src/core/infrastructure/database/prisma.service.ts +26 -0
  24. package/infra/prisma-postgres/infra.json +60 -0
  25. package/infra/rabbitmq/docker.service.yml +16 -0
  26. package/infra/rabbitmq/files/src/core/config/messaging.env.ts +29 -0
  27. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/inline-job-queue.spec.ts +60 -0
  28. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/inline-job-queue.ts +33 -0
  29. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/job-failure.spec.ts +23 -0
  30. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/job-failure.ts +19 -0
  31. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/job-payload-cipher.spec.ts +80 -0
  32. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/job-payload-cipher.ts +66 -0
  33. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/job-queue.boundary.spec.ts +34 -0
  34. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/job-queue.factory.ts +21 -0
  35. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/job-queue.module.spec.ts +75 -0
  36. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/job-queue.module.ts +30 -0
  37. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/job-queue.port.ts +59 -0
  38. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/rabbitmq-job-queue.spec.ts +510 -0
  39. package/infra/rabbitmq/files/src/core/infrastructure/messaging/jobs/rabbitmq-job-queue.ts +353 -0
  40. package/infra/rabbitmq/files/src/core/infrastructure/messaging/rabbitmq/rabbitmq.module.ts +9 -0
  41. package/infra/rabbitmq/files/src/core/infrastructure/messaging/rabbitmq/rabbitmq.service.spec.ts +289 -0
  42. package/infra/rabbitmq/files/src/core/infrastructure/messaging/rabbitmq/rabbitmq.service.ts +207 -0
  43. package/infra/rabbitmq/infra.json +55 -0
  44. package/infra/redis/docker.service.yml +10 -0
  45. package/infra/redis/files/src/core/http/decorators/rate-limit.decorator.ts +26 -0
  46. package/infra/redis/files/src/core/http/interceptors/rate-limit.interceptor.spec.ts +180 -0
  47. package/infra/redis/files/src/core/http/interceptors/rate-limit.interceptor.ts +105 -0
  48. package/infra/redis/files/src/core/infrastructure/redis/cache.constants.ts +67 -0
  49. package/infra/redis/files/src/core/infrastructure/redis/idempotency/idempotency.module.ts +9 -0
  50. package/infra/redis/files/src/core/infrastructure/redis/idempotency/idempotency.service.spec.ts +146 -0
  51. package/infra/redis/files/src/core/infrastructure/redis/idempotency/idempotency.service.ts +131 -0
  52. package/infra/redis/files/src/core/infrastructure/redis/lock/lock.module.ts +9 -0
  53. package/infra/redis/files/src/core/infrastructure/redis/lock/lock.service.integration-spec.ts +111 -0
  54. package/infra/redis/files/src/core/infrastructure/redis/lock/lock.service.ts +82 -0
  55. package/infra/redis/files/src/core/infrastructure/redis/rate-limit/rate-limit.module.ts +15 -0
  56. package/infra/redis/files/src/core/infrastructure/redis/rate-limit/rate-limit.service.spec.ts +45 -0
  57. package/infra/redis/files/src/core/infrastructure/redis/rate-limit/rate-limit.service.ts +38 -0
  58. package/infra/redis/files/src/core/infrastructure/redis/redis.module.ts +9 -0
  59. package/infra/redis/files/src/core/infrastructure/redis/redis.service.ts +196 -0
  60. package/infra/redis/infra.json +53 -0
  61. package/infra/storage-cloudinary/files/src/core/config/storage.env.ts +43 -0
  62. package/infra/storage-cloudinary/files/src/core/infrastructure/storage/cloudinary-image-storage.spec.ts +275 -0
  63. package/infra/storage-cloudinary/files/src/core/infrastructure/storage/cloudinary-image-storage.ts +158 -0
  64. package/infra/storage-cloudinary/files/src/core/infrastructure/storage/image-storage.factory.ts +30 -0
  65. package/infra/storage-cloudinary/files/src/core/infrastructure/storage/image-storage.module.ts +17 -0
  66. package/infra/storage-cloudinary/files/src/core/infrastructure/storage/image-storage.port.ts +38 -0
  67. package/infra/storage-cloudinary/files/src/core/infrastructure/storage/image-type.spec.ts +48 -0
  68. package/infra/storage-cloudinary/files/src/core/infrastructure/storage/image-type.ts +25 -0
  69. package/infra/storage-cloudinary/files/src/core/infrastructure/storage/unconfigured-image-storage.ts +19 -0
  70. package/infra/storage-cloudinary/infra.json +73 -0
  71. package/lib/fs-utils.mjs +115 -0
  72. package/lib/registry.mjs +50 -0
  73. package/lib/scaffold.mjs +157 -0
  74. package/package.json +37 -0
  75. package/templates/nest-ddd/files/.env.example +5 -0
  76. package/templates/nest-ddd/files/.husky/commit-msg +1 -0
  77. package/templates/nest-ddd/files/.husky/pre-commit +1 -0
  78. package/templates/nest-ddd/files/.oxlintrc.json +10 -0
  79. package/templates/nest-ddd/files/.prettierrc +4 -0
  80. package/templates/nest-ddd/files/_gitignore +40 -0
  81. package/templates/nest-ddd/files/commitlint.config.js +3 -0
  82. package/templates/nest-ddd/files/nest-cli.json +8 -0
  83. package/templates/nest-ddd/files/package.json +66 -0
  84. package/templates/nest-ddd/files/plop-templates/command/command.ts.hbs +5 -0
  85. package/templates/nest-ddd/files/plop-templates/command/handler.ts.hbs +30 -0
  86. package/templates/nest-ddd/files/plop-templates/module/application/commands/create-{{kebab}}.command.ts.hbs +5 -0
  87. package/templates/nest-ddd/files/plop-templates/module/application/commands/create-{{kebab}}.handler.spec.ts.hbs +35 -0
  88. package/templates/nest-ddd/files/plop-templates/module/application/commands/create-{{kebab}}.handler.ts.hbs +25 -0
  89. package/templates/nest-ddd/files/plop-templates/module/application/index.ts.hbs +5 -0
  90. package/templates/nest-ddd/files/plop-templates/module/application/queries/get-{{kebab}}-by-id.handler.ts.hbs +29 -0
  91. package/templates/nest-ddd/files/plop-templates/module/application/queries/get-{{kebab}}-by-id.query.ts.hbs +5 -0
  92. package/templates/nest-ddd/files/plop-templates/module/domain/entities/{{kebab}}.entity.ts.hbs +37 -0
  93. package/templates/nest-ddd/files/plop-templates/module/domain/events/{{kebab}}-created.event.ts.hbs +10 -0
  94. package/templates/nest-ddd/files/plop-templates/module/domain/exceptions/{{kebab}}.exception.ts.hbs +11 -0
  95. package/templates/nest-ddd/files/plop-templates/module/domain/index.ts.hbs +4 -0
  96. package/templates/nest-ddd/files/plop-templates/module/domain/repositories/{{kebab}}.repository.ts.hbs +7 -0
  97. package/templates/nest-ddd/files/plop-templates/module/presentation/controllers/{{kebab}}.controller.ts.hbs +53 -0
  98. package/templates/nest-ddd/files/plop-templates/module/presentation/dtos/create-{{kebab}}.dto.ts.hbs +10 -0
  99. package/templates/nest-ddd/files/plop-templates/module/presentation/dtos/{{kebab}}-response.dto.ts.hbs +15 -0
  100. package/templates/nest-ddd/files/plop-templates/module/presentation/index.ts.hbs +4 -0
  101. package/templates/nest-ddd/files/plop-templates/module/presentation/mappers/{{kebab}}.mapper.ts.hbs +13 -0
  102. package/templates/nest-ddd/files/plop-templates/module/{{kebab}}.module.ts.hbs +40 -0
  103. package/templates/nest-ddd/files/plop-templates/persistence-memory/infrastructure/database/repositories/in-memory-{{kebab}}.repository.ts.hbs +17 -0
  104. package/templates/nest-ddd/files/plop-templates/persistence-memory/infrastructure/index.ts.hbs +1 -0
  105. package/templates/nest-ddd/files/plop-templates/persistence-prisma/infrastructure/database/mappers/{{kebab}}.mapper.ts.hbs +29 -0
  106. package/templates/nest-ddd/files/plop-templates/persistence-prisma/infrastructure/database/repositories/prisma-{{kebab}}.repository.ts.hbs +28 -0
  107. package/templates/nest-ddd/files/plop-templates/persistence-prisma/infrastructure/index.ts.hbs +2 -0
  108. package/templates/nest-ddd/files/plop-templates/query/handler.ts.hbs +28 -0
  109. package/templates/nest-ddd/files/plop-templates/query/query.ts.hbs +5 -0
  110. package/templates/nest-ddd/files/plop-templates/snippets/error-codes.hbs +14 -0
  111. package/templates/nest-ddd/files/plop-templates/snippets/prisma-model.hbs +7 -0
  112. package/templates/nest-ddd/files/plopfile.mjs +186 -0
  113. package/templates/nest-ddd/files/src/app.controller.ts +13 -0
  114. package/templates/nest-ddd/files/src/app.module.ts +26 -0
  115. package/templates/nest-ddd/files/src/core/config/app.config.ts +9 -0
  116. package/templates/nest-ddd/files/src/core/config/env.validation.ts +30 -0
  117. package/templates/nest-ddd/files/src/core/domain/base/aggregate-root.base.ts +18 -0
  118. package/templates/nest-ddd/files/src/core/domain/base/domain-event.base.ts +7 -0
  119. package/templates/nest-ddd/files/src/core/domain/base/entity.base.ts +36 -0
  120. package/templates/nest-ddd/files/src/core/domain/base/value-object.base.ts +17 -0
  121. package/templates/nest-ddd/files/src/core/domain/exceptions/domain.exception.ts +16 -0
  122. package/templates/nest-ddd/files/src/core/domain/exceptions/error-codes.ts +28 -0
  123. package/templates/nest-ddd/files/src/core/domain/types/pagination.ts +9 -0
  124. package/templates/nest-ddd/files/src/core/http/decorators/client-info.decorator.ts +24 -0
  125. package/templates/nest-ddd/files/src/core/http/decorators/current-user.decorator.ts +19 -0
  126. package/templates/nest-ddd/files/src/core/http/dtos/paginated-response.dto.ts +29 -0
  127. package/templates/nest-ddd/files/src/core/http/dtos/pagination-query.dto.ts +17 -0
  128. package/templates/nest-ddd/files/src/core/http/filters/all-exceptions.filter.spec.ts +163 -0
  129. package/templates/nest-ddd/files/src/core/http/filters/all-exceptions.filter.ts +97 -0
  130. package/templates/nest-ddd/files/src/core/http/guards/max-body-size.guard.spec.ts +47 -0
  131. package/templates/nest-ddd/files/src/core/http/guards/max-body-size.guard.ts +31 -0
  132. package/templates/nest-ddd/files/src/core/http/interceptors/logging.interceptor.ts +37 -0
  133. package/templates/nest-ddd/files/src/core/http/interceptors/response.interceptor.ts +53 -0
  134. package/templates/nest-ddd/files/src/core/http/logging/app.logger.ts +34 -0
  135. package/templates/nest-ddd/files/src/core/index.ts +33 -0
  136. package/templates/nest-ddd/files/src/core/utils/correlation-id.util.spec.ts +29 -0
  137. package/templates/nest-ddd/files/src/core/utils/correlation-id.util.ts +15 -0
  138. package/templates/nest-ddd/files/src/core/utils/hash.util.spec.ts +61 -0
  139. package/templates/nest-ddd/files/src/core/utils/hash.util.ts +47 -0
  140. package/templates/nest-ddd/files/src/core/utils/snowflake.util.spec.ts +40 -0
  141. package/templates/nest-ddd/files/src/core/utils/snowflake.util.ts +99 -0
  142. package/templates/nest-ddd/files/src/core/utils/string.util.spec.ts +20 -0
  143. package/templates/nest-ddd/files/src/core/utils/string.util.ts +21 -0
  144. package/templates/nest-ddd/files/src/core/utils/time.util.spec.ts +25 -0
  145. package/templates/nest-ddd/files/src/core/utils/time.util.ts +25 -0
  146. package/templates/nest-ddd/files/src/core/utils/ttl.util.ts +22 -0
  147. package/templates/nest-ddd/files/src/main.ts +61 -0
  148. package/templates/nest-ddd/files/test/setup.ts +6 -0
  149. package/templates/nest-ddd/files/tsconfig.build.json +9 -0
  150. package/templates/nest-ddd/files/tsconfig.json +28 -0
  151. package/templates/nest-ddd/files/vitest.config.e2e.ts +15 -0
  152. package/templates/nest-ddd/files/vitest.config.ts +16 -0
  153. package/templates/nest-ddd/template.json +8 -0
  154. package/templates/node-express/files/.env.example +28 -0
  155. package/templates/node-express/files/docker-compose.yml +52 -0
  156. package/templates/node-express/files/package-lock.json +11880 -0
  157. package/templates/node-express/files/package.json +43 -0
  158. package/templates/node-express/files/server.js +13 -0
  159. package/templates/node-express/files/src/app.js +72 -0
  160. package/templates/node-express/files/src/auth/authUtils.js +116 -0
  161. package/templates/node-express/files/src/auth/checkAuth.js +61 -0
  162. package/templates/node-express/files/src/bucket_pattern/comment.model.js +27 -0
  163. package/templates/node-express/files/src/bucket_pattern/comment.service.js +50 -0
  164. package/templates/node-express/files/src/config/config.aws-s3.js +28 -0
  165. package/templates/node-express/files/src/config/config.cloudinary.js +13 -0
  166. package/templates/node-express/files/src/config/config.mongodb.js +25 -0
  167. package/templates/node-express/files/src/config/config.multer.js +25 -0
  168. package/templates/node-express/files/src/config/config.nodemailer.js +16 -0
  169. package/templates/node-express/files/src/controllers/access.controller.js +49 -0
  170. package/templates/node-express/files/src/controllers/cart.controller.js +38 -0
  171. package/templates/node-express/files/src/controllers/checkout.controller.js +15 -0
  172. package/templates/node-express/files/src/controllers/comment.controller.js +29 -0
  173. package/templates/node-express/files/src/controllers/discount.controller.js +68 -0
  174. package/templates/node-express/files/src/controllers/email.controller.js +16 -0
  175. package/templates/node-express/files/src/controllers/inventory.controller.js +15 -0
  176. package/templates/node-express/files/src/controllers/notification.controller.js +15 -0
  177. package/templates/node-express/files/src/controllers/product.controller.js +175 -0
  178. package/templates/node-express/files/src/controllers/profile.controller.js +65 -0
  179. package/templates/node-express/files/src/controllers/rbac.controller.js +41 -0
  180. package/templates/node-express/files/src/controllers/template.controller.js +20 -0
  181. package/templates/node-express/files/src/controllers/upload.controller.js +56 -0
  182. package/templates/node-express/files/src/controllers/user.controller.js +36 -0
  183. package/templates/node-express/files/src/core/error.response.js +84 -0
  184. package/templates/node-express/files/src/core/success.response.js +44 -0
  185. package/templates/node-express/files/src/database/init.mongodb.level0.js +16 -0
  186. package/templates/node-express/files/src/database/init.mongodb.level1.js +36 -0
  187. package/templates/node-express/files/src/database/init.redis.js +119 -0
  188. package/templates/node-express/files/src/helpers/asyncHandler.js +7 -0
  189. package/templates/node-express/files/src/helpers/check.connect.js +29 -0
  190. package/templates/node-express/files/src/keys/private_key.pem +28 -0
  191. package/templates/node-express/files/src/keys/public_key.pem +9 -0
  192. package/templates/node-express/files/src/logger/discord.log.js +26 -0
  193. package/templates/node-express/files/src/logger/discord.log.v2.js +56 -0
  194. package/templates/node-express/files/src/logger/mylogger.log.js +130 -0
  195. package/templates/node-express/files/src/logger/winston.log.js +27 -0
  196. package/templates/node-express/files/src/middlewares/index.js +21 -0
  197. package/templates/node-express/files/src/middlewares/rbac.js +37 -0
  198. package/templates/node-express/files/src/middlewares/role.middleware.js +12 -0
  199. package/templates/node-express/files/src/models/KeyToken.model.js +37 -0
  200. package/templates/node-express/files/src/models/apiKey.model.js +31 -0
  201. package/templates/node-express/files/src/models/cart.model.js +47 -0
  202. package/templates/node-express/files/src/models/comment.model.js +43 -0
  203. package/templates/node-express/files/src/models/discount.model.js +92 -0
  204. package/templates/node-express/files/src/models/inventory.model.js +42 -0
  205. package/templates/node-express/files/src/models/notification.model.js +52 -0
  206. package/templates/node-express/files/src/models/order.model.js +68 -0
  207. package/templates/node-express/files/src/models/otp.model.js +32 -0
  208. package/templates/node-express/files/src/models/product.model.js +144 -0
  209. package/templates/node-express/files/src/models/repository/cart.repo.js +71 -0
  210. package/templates/node-express/files/src/models/repository/discount.repo.js +63 -0
  211. package/templates/node-express/files/src/models/repository/inventory.repo.js +45 -0
  212. package/templates/node-express/files/src/models/repository/product.repo.js +145 -0
  213. package/templates/node-express/files/src/models/repository/shop.repo.js +18 -0
  214. package/templates/node-express/files/src/models/repository/spu.repo.js +18 -0
  215. package/templates/node-express/files/src/models/repository/user.repo.js +20 -0
  216. package/templates/node-express/files/src/models/resource.model.js +32 -0
  217. package/templates/node-express/files/src/models/role.model.js +57 -0
  218. package/templates/node-express/files/src/models/shop.model.js +43 -0
  219. package/templates/node-express/files/src/models/sku.model.js +81 -0
  220. package/templates/node-express/files/src/models/spu.model.js +101 -0
  221. package/templates/node-express/files/src/models/template.model.js +25 -0
  222. package/templates/node-express/files/src/models/user.model.js +82 -0
  223. package/templates/node-express/files/src/postman/access.post.http +22 -0
  224. package/templates/node-express/files/src/routes/access/index.route.js +21 -0
  225. package/templates/node-express/files/src/routes/cart/index.route.js +20 -0
  226. package/templates/node-express/files/src/routes/checkout/index.route.js +10 -0
  227. package/templates/node-express/files/src/routes/comment/index.route.js +16 -0
  228. package/templates/node-express/files/src/routes/discount/index.route.js +26 -0
  229. package/templates/node-express/files/src/routes/index.route.js +27 -0
  230. package/templates/node-express/files/src/routes/inventory/index.route.js +11 -0
  231. package/templates/node-express/files/src/routes/notification/index.route.js +14 -0
  232. package/templates/node-express/files/src/routes/product/index.route.js +38 -0
  233. package/templates/node-express/files/src/routes/profile/index.route.js +15 -0
  234. package/templates/node-express/files/src/routes/rbac/index.route.js +21 -0
  235. package/templates/node-express/files/src/routes/template/index.route.js +11 -0
  236. package/templates/node-express/files/src/routes/upload/index.route.js +28 -0
  237. package/templates/node-express/files/src/routes/user/index.route.js +13 -0
  238. package/templates/node-express/files/src/services/access.service.js +257 -0
  239. package/templates/node-express/files/src/services/apiKey.service.js +15 -0
  240. package/templates/node-express/files/src/services/cart.service.js +115 -0
  241. package/templates/node-express/files/src/services/checkout.service.js +218 -0
  242. package/templates/node-express/files/src/services/comment.service.js +185 -0
  243. package/templates/node-express/files/src/services/discount.service.js +282 -0
  244. package/templates/node-express/files/src/services/email.service.js +71 -0
  245. package/templates/node-express/files/src/services/inventory.service.js +35 -0
  246. package/templates/node-express/files/src/services/keyToken.service.js +77 -0
  247. package/templates/node-express/files/src/services/notification.service.js +58 -0
  248. package/templates/node-express/files/src/services/otp.service.js +36 -0
  249. package/templates/node-express/files/src/services/product.service.js +228 -0
  250. package/templates/node-express/files/src/services/rbac.service.js +143 -0
  251. package/templates/node-express/files/src/services/redis.service.js +57 -0
  252. package/templates/node-express/files/src/services/redisPubSub.service.js +72 -0
  253. package/templates/node-express/files/src/services/shop.service.js +21 -0
  254. package/templates/node-express/files/src/services/sku.service.js +52 -0
  255. package/templates/node-express/files/src/services/spu.service.js +89 -0
  256. package/templates/node-express/files/src/services/template.service.js +41 -0
  257. package/templates/node-express/files/src/services/upload.s3.service.js +55 -0
  258. package/templates/node-express/files/src/services/upload.service.js +74 -0
  259. package/templates/node-express/files/src/services/user.service.js +92 -0
  260. package/templates/node-express/files/src/temp/1734420604996-ci-cd.png +0 -0
  261. package/templates/node-express/files/src/temp/1734421222213-cover-fb.jpg +0 -0
  262. package/templates/node-express/files/src/temp/1734421222214-cover-fb-01.jpg +0 -0
  263. package/templates/node-express/files/src/temp/1734421293437-cover-fb-01.jpg +0 -0
  264. package/templates/node-express/files/src/temp/1734421293437-cover-fb.jpg +0 -0
  265. package/templates/node-express/files/src/tests/inventory.test.js +21 -0
  266. package/templates/node-express/files/src/tests/message_queue/kafka/consumer.js +23 -0
  267. package/templates/node-express/files/src/tests/message_queue/kafka/producer.js +21 -0
  268. package/templates/node-express/files/src/tests/message_queue/rabbitMQ/consumer.js +28 -0
  269. package/templates/node-express/files/src/tests/message_queue/rabbitMQ/ordered.consumer.js +26 -0
  270. package/templates/node-express/files/src/tests/message_queue/rabbitMQ/ordered.producer.js +31 -0
  271. package/templates/node-express/files/src/tests/message_queue/rabbitMQ/producer.js +26 -0
  272. package/templates/node-express/files/src/tests/message_queue/rabbitMQ/producerDLX.js +44 -0
  273. package/templates/node-express/files/src/tests/mysql/connect.js +59 -0
  274. package/templates/node-express/files/src/tests/product.test.js +14 -0
  275. package/templates/node-express/files/src/utils/index.js +78 -0
  276. package/templates/node-express/files/src/utils/reasonPhrases.js +344 -0
  277. package/templates/node-express/files/src/utils/statusCodes.js +344 -0
  278. package/templates/node-express/files/src/utils/template.html.js +38 -0
  279. package/templates/node-express/template.json +11 -0
  280. package/templates/react-fsd/files/index.html +13 -0
  281. package/templates/react-fsd/files/package.json +15 -0
  282. package/templates/react-fsd/files/public/favicon.svg +1 -0
  283. package/templates/react-fsd/files/public/icons.svg +24 -0
  284. package/templates/react-fsd/files/src/assets/hero.png +0 -0
  285. package/templates/react-fsd/files/src/assets/typescript.svg +1 -0
  286. package/templates/react-fsd/files/src/assets/vite.svg +1 -0
  287. package/templates/react-fsd/files/src/counter.ts +9 -0
  288. package/templates/react-fsd/files/src/main.ts +60 -0
  289. package/templates/react-fsd/files/src/style.css +296 -0
  290. package/templates/react-fsd/files/tsconfig.json +24 -0
  291. package/templates/react-fsd/template.json +11 -0
  292. package/templates/react-monolithic/files/index.html +13 -0
  293. package/templates/react-monolithic/files/package.json +15 -0
  294. package/templates/react-monolithic/files/public/favicon.svg +1 -0
  295. package/templates/react-monolithic/files/public/icons.svg +24 -0
  296. package/templates/react-monolithic/files/src/assets/hero.png +0 -0
  297. package/templates/react-monolithic/files/src/assets/typescript.svg +1 -0
  298. package/templates/react-monolithic/files/src/assets/vite.svg +1 -0
  299. package/templates/react-monolithic/files/src/counter.ts +9 -0
  300. package/templates/react-monolithic/files/src/main.ts +60 -0
  301. package/templates/react-monolithic/files/src/style.css +296 -0
  302. package/templates/react-monolithic/files/tsconfig.json +24 -0
  303. package/templates/react-monolithic/template.json +11 -0
  304. package/templates/vue-fsd/files/index.html +13 -0
  305. package/templates/vue-fsd/files/package.json +15 -0
  306. package/templates/vue-fsd/files/public/favicon.svg +1 -0
  307. package/templates/vue-fsd/files/public/icons.svg +24 -0
  308. package/templates/vue-fsd/files/src/assets/hero.png +0 -0
  309. package/templates/vue-fsd/files/src/assets/typescript.svg +1 -0
  310. package/templates/vue-fsd/files/src/assets/vite.svg +1 -0
  311. package/templates/vue-fsd/files/src/counter.ts +9 -0
  312. package/templates/vue-fsd/files/src/main.ts +60 -0
  313. package/templates/vue-fsd/files/src/style.css +296 -0
  314. package/templates/vue-fsd/files/tsconfig.json +24 -0
  315. package/templates/vue-fsd/template.json +11 -0
  316. package/templates/vue-monolithic/files/index.html +13 -0
  317. package/templates/vue-monolithic/files/package.json +15 -0
  318. package/templates/vue-monolithic/files/public/favicon.svg +1 -0
  319. package/templates/vue-monolithic/files/public/icons.svg +24 -0
  320. package/templates/vue-monolithic/files/src/assets/hero.png +0 -0
  321. package/templates/vue-monolithic/files/src/assets/typescript.svg +1 -0
  322. package/templates/vue-monolithic/files/src/assets/vite.svg +1 -0
  323. package/templates/vue-monolithic/files/src/counter.ts +9 -0
  324. package/templates/vue-monolithic/files/src/main.ts +60 -0
  325. package/templates/vue-monolithic/files/src/style.css +296 -0
  326. package/templates/vue-monolithic/files/tsconfig.json +24 -0
  327. package/templates/vue-monolithic/template.json +11 -0
@@ -0,0 +1,29 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { PaginatedResult } from '../../domain/types/pagination.js';
3
+
4
+ export class PaginatedMetaDto {
5
+ @ApiProperty()
6
+ hasMore: boolean;
7
+
8
+ @ApiProperty({ nullable: true })
9
+ nextCursor: string | null;
10
+ }
11
+
12
+ export class PaginatedResponseDto<T> {
13
+ // @ApiProperty for data is usually defined dynamically per controller using @ApiExtraModels
14
+ // or a custom decorator. We'll leave the basic structure here.
15
+ data: T[];
16
+
17
+ @ApiProperty()
18
+ meta: PaginatedMetaDto;
19
+
20
+ static map<T, U>(
21
+ result: PaginatedResult<T>,
22
+ mapperFn: (item: T) => U,
23
+ ): PaginatedResponseDto<U> {
24
+ const response = new PaginatedResponseDto<U>();
25
+ response.data = result.data.map(mapperFn);
26
+ response.meta = result.meta;
27
+ return response;
28
+ }
29
+ }
@@ -0,0 +1,17 @@
1
+ import { ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { Type } from 'class-transformer';
3
+ import { IsInt, IsOptional, IsString, Min } from 'class-validator';
4
+
5
+ export class PaginationQueryDto {
6
+ @ApiPropertyOptional({ default: 20, description: 'Number of items per page' })
7
+ @IsOptional()
8
+ @Type(() => Number)
9
+ @IsInt()
10
+ @Min(1)
11
+ limit?: number = 20;
12
+
13
+ @ApiPropertyOptional({ description: 'Cursor for pagination' })
14
+ @IsOptional()
15
+ @IsString()
16
+ cursor?: string;
17
+ }
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ ArgumentsHost,
4
+ BadRequestException,
5
+ HttpStatus,
6
+ NotFoundException,
7
+ } from '@nestjs/common';
8
+ import type { ClsService } from 'nestjs-cls';
9
+ import { AllExceptionsFilter } from './all-exceptions.filter.js';
10
+ import { DomainException } from '../../domain/exceptions/domain.exception.js';
11
+ import {
12
+ AuthErrorCodes,
13
+ CoreErrorCodes,
14
+ } from '../../domain/exceptions/error-codes.js';
15
+
16
+ describe('AllExceptionsFilter', () => {
17
+ let filter: AllExceptionsFilter;
18
+ let json: ReturnType<typeof vi.fn>;
19
+ let status: ReturnType<typeof vi.fn>;
20
+ let setHeader: ReturnType<typeof vi.fn>;
21
+ let host: ArgumentsHost;
22
+
23
+ const respond = (exception: unknown) => {
24
+ filter.catch(exception, host);
25
+ return {
26
+ status: status.mock.calls[0][0] as number,
27
+ body: json.mock.calls[0][0],
28
+ };
29
+ };
30
+
31
+ beforeEach(() => {
32
+ json = vi.fn();
33
+ status = vi.fn().mockReturnValue({ json });
34
+ setHeader = vi.fn();
35
+ host = {
36
+ switchToHttp: () => ({
37
+ getResponse: () => ({ status, setHeader }),
38
+ getRequest: () => ({ method: 'POST', url: '/api/v1/auth/register' }),
39
+ }),
40
+ } as unknown as ArgumentsHost;
41
+ filter = new AllExceptionsFilter({
42
+ getId: () => 'trace-1',
43
+ } as unknown as ClsService);
44
+ });
45
+
46
+ it('keeps the message and code of a DomainException', () => {
47
+ const { status: s, body } = respond(
48
+ new DomainException(
49
+ AuthErrorCodes.INVALID_CREDENTIALS,
50
+ 'Invalid email or password',
51
+ ),
52
+ );
53
+
54
+ expect(s).toBe(HttpStatus.UNAUTHORIZED);
55
+ expect(body).toMatchObject({
56
+ code: AuthErrorCodes.INVALID_CREDENTIALS.code,
57
+ message: 'Invalid email or password',
58
+ traceId: 'trace-1',
59
+ });
60
+ });
61
+
62
+ it('tells the client how long to wait: in the body and in the Retry-After header', () => {
63
+ const { status: s, body } = respond(
64
+ new DomainException(AuthErrorCodes.RATE_LIMIT_EXCEEDED, {
65
+ retryAfterSeconds: 217.2,
66
+ }),
67
+ );
68
+
69
+ expect(s).toBe(HttpStatus.TOO_MANY_REQUESTS);
70
+ expect(body.errors).toEqual({ retryAfterSeconds: 217.2 });
71
+ expect(setHeader).toHaveBeenCalledWith('Retry-After', '218');
72
+ });
73
+
74
+ it('sets no Retry-After header when the error carries no wait time', () => {
75
+ respond(new DomainException(AuthErrorCodes.INVALID_CREDENTIALS, 'nope'));
76
+
77
+ expect(setHeader).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('keeps the message of an HttpException', () => {
81
+ const { status: s, body } = respond(
82
+ new NotFoundException('Product not found'),
83
+ );
84
+
85
+ expect(s).toBe(HttpStatus.NOT_FOUND);
86
+ expect(body).toMatchObject({
87
+ code: 'HTTP_404',
88
+ message: 'Product not found',
89
+ });
90
+ });
91
+
92
+ it('turns class-validator failures into a VALIDATION_ERROR with the field errors', () => {
93
+ const { status: s, body } = respond(
94
+ new BadRequestException({ message: ['email must be an email'] }),
95
+ );
96
+
97
+ expect(s).toBe(HttpStatus.BAD_REQUEST);
98
+ expect(body).toMatchObject({
99
+ code: CoreErrorCodes.VALIDATION_ERROR.code,
100
+ errors: ['email must be an email'],
101
+ });
102
+ });
103
+
104
+ describe('unexpected errors', () => {
105
+ it('never puts the raw error message in the response (it can carry SQL / schema details)', () => {
106
+ const prismaLike = new Error(
107
+ 'Unique constraint failed on the fields: (`provider`,`identifier`)',
108
+ );
109
+
110
+ const { status: s, body } = respond(prismaLike);
111
+
112
+ expect(s).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
113
+ expect(body.message).toBe(CoreErrorCodes.SYSTEM_ERROR.message);
114
+ expect(body.code).toBe(CoreErrorCodes.SYSTEM_ERROR.code);
115
+ expect(JSON.stringify(body)).not.toContain('provider');
116
+ expect(JSON.stringify(body)).not.toContain('identifier');
117
+ });
118
+
119
+ it('never puts the stack trace in the response', () => {
120
+ const { body } = respond(new Error('boom'));
121
+
122
+ expect(body).not.toHaveProperty('stack');
123
+ });
124
+
125
+ it('handles a thrown non-Error value', () => {
126
+ const { status: s, body } = respond('just a string');
127
+
128
+ expect(s).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
129
+ expect(body.message).toBe(CoreErrorCodes.SYSTEM_ERROR.message);
130
+ });
131
+ });
132
+
133
+ describe('upload errors from multer', () => {
134
+ const multerError = (code: string, message: string) =>
135
+ Object.assign(new Error(message), { name: 'MulterError', code });
136
+
137
+ it('answers 413 for a file that is too large', () => {
138
+ const { status: s, body } = respond(
139
+ multerError('LIMIT_FILE_SIZE', 'File too large'),
140
+ );
141
+
142
+ expect(s).toBe(HttpStatus.PAYLOAD_TOO_LARGE);
143
+ expect(body.message).toBe('File too large');
144
+ });
145
+
146
+ it.each([
147
+ ['LIMIT_UNEXPECTED_FILE', 'Unexpected file field'],
148
+ ['LIMIT_FILE_COUNT', 'Too many files'],
149
+ ['INVALID_FIELD_NAME', 'Invalid field name'],
150
+ ])('answers 400, not 500, for %s', (code, message) => {
151
+ const { status: s, body } = respond(multerError(code, message));
152
+
153
+ expect(s).toBe(HttpStatus.BAD_REQUEST);
154
+ expect(body).toMatchObject({ code: 'HTTP_400', message });
155
+ });
156
+
157
+ it('does not mistake an ordinary error for an upload error', () => {
158
+ const { status: s } = respond(new Error('MulterError'));
159
+
160
+ expect(s).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,97 @@
1
+ import {
2
+ ExceptionFilter,
3
+ Catch,
4
+ ArgumentsHost,
5
+ HttpException,
6
+ HttpStatus,
7
+ Logger,
8
+ } from '@nestjs/common';
9
+ import { Response, Request } from 'express';
10
+ import { ClsService } from 'nestjs-cls';
11
+ import { DomainException } from '../../domain/exceptions/domain.exception.js';
12
+ import { CoreErrorCodes } from '../../domain/exceptions/error-codes.js';
13
+ import { randomUUID } from 'crypto';
14
+
15
+ @Catch()
16
+ export class AllExceptionsFilter implements ExceptionFilter {
17
+ private readonly logger = new Logger(AllExceptionsFilter.name);
18
+
19
+ constructor(private readonly cls: ClsService) {}
20
+
21
+ catch(exception: unknown, host: ArgumentsHost) {
22
+ const ctx = host.switchToHttp();
23
+ const response = ctx.getResponse<Response>();
24
+ const request = ctx.getRequest<Request>();
25
+ const traceId = this.cls.getId() || randomUUID();
26
+
27
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
28
+ let message = CoreErrorCodes.SYSTEM_ERROR.message;
29
+ let code = CoreErrorCodes.SYSTEM_ERROR.code;
30
+ let errors: any = null;
31
+
32
+ if (exception instanceof HttpException) {
33
+ status = exception.getStatus();
34
+ const res = exception.getResponse() as any;
35
+
36
+ if (status === HttpStatus.BAD_REQUEST && Array.isArray(res.message)) {
37
+ message = CoreErrorCodes.VALIDATION_ERROR.message;
38
+ code = CoreErrorCodes.VALIDATION_ERROR.code;
39
+ errors = res.message;
40
+ } else {
41
+ message = res.message || exception.message;
42
+ code = `HTTP_${status}`;
43
+ }
44
+ } else if (exception instanceof DomainException) {
45
+ status = exception.status;
46
+ message = exception.message;
47
+ code = exception.code;
48
+ errors = exception.details || null;
49
+ // A refusal that says when to come back also says so in the standard header
50
+ const retryAfter = (exception.details as { retryAfterSeconds?: unknown })
51
+ ?.retryAfterSeconds;
52
+ if (typeof retryAfter === 'number' && retryAfter > 0) {
53
+ response.setHeader('Retry-After', String(Math.ceil(retryAfter)));
54
+ }
55
+ } else if (isMulterError(exception)) {
56
+ // A bad upload (wrong field name, too many files, too big). Nest maps only some of
57
+ // multer's messages, so the rest would surface as a 500; they are all the sender's doing.
58
+ status =
59
+ exception.code === 'LIMIT_FILE_SIZE'
60
+ ? HttpStatus.PAYLOAD_TOO_LARGE
61
+ : HttpStatus.BAD_REQUEST;
62
+ message = exception.message;
63
+ code = `HTTP_${status}`;
64
+ }
65
+ // Anything else is an unexpected failure (Prisma, Redis, a bug...). Its message
66
+ // can carry SQL, table/column names or connection details, so the client only
67
+ // gets the generic 500 message; the real error is logged below.
68
+
69
+ // Ghi log lỗi, AppLogger sẽ tự động thêm [TraceId-...] ở đầu nên không cần thêm thủ công ở đây
70
+ this.logger.error(
71
+ `${request.method} ${request.url} - ${status} - ${message}`,
72
+ exception instanceof Error ? exception.stack : '',
73
+ );
74
+
75
+ response.status(status).json({
76
+ statusCode: status,
77
+ code,
78
+ message,
79
+ ...(errors && { errors }),
80
+ traceId,
81
+ timestamp: new Date().toISOString(),
82
+ });
83
+ }
84
+ }
85
+
86
+ interface MulterErrorLike extends Error {
87
+ code: string;
88
+ }
89
+
90
+ /** multer's own error, recognised by name so this file does not depend on the multer package. */
91
+ function isMulterError(exception: unknown): exception is MulterErrorLike {
92
+ return (
93
+ exception instanceof Error &&
94
+ exception.name === 'MulterError' &&
95
+ typeof (exception as { code?: unknown }).code === 'string'
96
+ );
97
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ExecutionContext, PayloadTooLargeException } from '@nestjs/common';
3
+ import { MaxBodySizeGuard } from './max-body-size.guard.js';
4
+
5
+ const contextWith = (headers: Record<string, string>) =>
6
+ ({
7
+ switchToHttp: () => ({ getRequest: () => ({ headers }) }),
8
+ }) as unknown as ExecutionContext;
9
+
10
+ describe('MaxBodySizeGuard', () => {
11
+ const guard = new (MaxBodySizeGuard(1000))();
12
+
13
+ it('lets a request through that announces a size within the limit, exactly at it included', () => {
14
+ expect(guard.canActivate(contextWith({ 'content-length': '10' }))).toBe(
15
+ true,
16
+ );
17
+ expect(guard.canActivate(contextWith({ 'content-length': '1000' }))).toBe(
18
+ true,
19
+ );
20
+ });
21
+
22
+ it('refuses with 413, before anything is read, a request that announces more', () => {
23
+ expect(() =>
24
+ guard.canActivate(contextWith({ 'content-length': '1001' })),
25
+ ).toThrow(PayloadTooLargeException);
26
+ });
27
+
28
+ it('lets a request through that announces no size (the upload limits still apply to it)', () => {
29
+ expect(guard.canActivate(contextWith({}))).toBe(true);
30
+ });
31
+
32
+ it('lets a request through whose size is not a number (it is the upload limits that handle it)', () => {
33
+ expect(guard.canActivate(contextWith({ 'content-length': 'abc' }))).toBe(
34
+ true,
35
+ );
36
+ });
37
+
38
+ it('each limit is its own: two guards do not share their number', () => {
39
+ const small = new (MaxBodySizeGuard(5))();
40
+ expect(() =>
41
+ small.canActivate(contextWith({ 'content-length': '6' })),
42
+ ).toThrow();
43
+ expect(guard.canActivate(contextWith({ 'content-length': '6' }))).toBe(
44
+ true,
45
+ );
46
+ });
47
+ });
@@ -0,0 +1,31 @@
1
+ import {
2
+ CanActivate,
3
+ ExecutionContext,
4
+ PayloadTooLargeException,
5
+ Type,
6
+ mixin,
7
+ } from '@nestjs/common';
8
+ import type { Request } from 'express';
9
+
10
+ /**
11
+ * Refuses a request whose announced size (the Content-Length header) is over the limit, before
12
+ * the body is read at all. An upload endpoint needs this: the upload library only stops a file
13
+ * after it started reading it, and on an error it still reads the rest of the body to the end.
14
+ * A request that does not announce its size is still bounded by the upload library's own limits.
15
+ *
16
+ * Usage: `@UseGuards(MaxBodySizeGuard(2 * 1024 * 1024 + 8 * 1024))`, a little above the largest
17
+ * file to leave room for the multipart framing.
18
+ */
19
+ export function MaxBodySizeGuard(maxBytes: number): Type<CanActivate> {
20
+ class MaxBodySizeGuardMixin implements CanActivate {
21
+ canActivate(context: ExecutionContext): boolean {
22
+ const request = context.switchToHttp().getRequest<Request>();
23
+ const announced = Number(request.headers['content-length']);
24
+ if (Number.isFinite(announced) && announced > maxBytes) {
25
+ throw new PayloadTooLargeException('Request body too large');
26
+ }
27
+ return true;
28
+ }
29
+ }
30
+ return mixin(MaxBodySizeGuardMixin);
31
+ }
@@ -0,0 +1,37 @@
1
+ import {
2
+ Injectable,
3
+ NestInterceptor,
4
+ ExecutionContext,
5
+ CallHandler,
6
+ Logger,
7
+ } from '@nestjs/common';
8
+ import { Observable } from 'rxjs';
9
+ import { tap } from 'rxjs/operators';
10
+ import { Request, Response } from 'express';
11
+
12
+ @Injectable()
13
+ export class LoggingInterceptor implements NestInterceptor {
14
+ private readonly logger = new Logger(LoggingInterceptor.name);
15
+
16
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
17
+ const ctx = context.switchToHttp();
18
+ const request = ctx.getRequest<Request>();
19
+ const response = ctx.getResponse<Response>();
20
+
21
+ const method = request.method;
22
+ const url = request.url;
23
+ const now = Date.now();
24
+
25
+ this.logger.log(`Incoming Request ${method} ${url}`);
26
+
27
+ return next.handle().pipe(
28
+ tap(() => {
29
+ const statusCode = response.statusCode;
30
+ const delay = Date.now() - now;
31
+ this.logger.log(
32
+ `Outgoing Response ${method} ${url} - ${statusCode} - ${delay}ms`,
33
+ );
34
+ }),
35
+ );
36
+ }
37
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ CallHandler,
3
+ ExecutionContext,
4
+ Injectable,
5
+ NestInterceptor,
6
+ } from '@nestjs/common';
7
+ import { Observable } from 'rxjs';
8
+ import { map } from 'rxjs/operators';
9
+
10
+ export interface StandardResponse<T> {
11
+ statusCode: number;
12
+ message: string;
13
+ data: T;
14
+ meta?: any;
15
+ }
16
+
17
+ @Injectable()
18
+ export class ResponseInterceptor<T> implements NestInterceptor<
19
+ T,
20
+ StandardResponse<T>
21
+ > {
22
+ intercept(
23
+ context: ExecutionContext,
24
+ next: CallHandler,
25
+ ): Observable<StandardResponse<T>> {
26
+ return next.handle().pipe(
27
+ map((result) => {
28
+ const statusCode = context.switchToHttp().getResponse().statusCode;
29
+
30
+ if (
31
+ result &&
32
+ typeof result === 'object' &&
33
+ 'data' in result &&
34
+ 'meta' in result
35
+ ) {
36
+ const paginatedResult = result as any;
37
+ return {
38
+ statusCode,
39
+ message: 'Success',
40
+ data: paginatedResult.data,
41
+ meta: paginatedResult.meta,
42
+ } as StandardResponse<T>;
43
+ }
44
+
45
+ return {
46
+ statusCode,
47
+ message: 'Success',
48
+ data: result,
49
+ } as StandardResponse<T>;
50
+ }),
51
+ );
52
+ }
53
+ }
@@ -0,0 +1,34 @@
1
+ import { ConsoleLogger, Injectable, LogLevel } from '@nestjs/common';
2
+ import { ClsServiceManager } from 'nestjs-cls';
3
+
4
+ @Injectable()
5
+ export class AppLogger extends ConsoleLogger {
6
+ protected formatMessage(
7
+ logLevel: LogLevel,
8
+ message: unknown,
9
+ pidMessage: string,
10
+ formattedLogLevel: string,
11
+ contextMessage: string,
12
+ timestampDiff: string,
13
+ ): string {
14
+ const cls = ClsServiceManager.getClsService();
15
+ let tracePrefix = '';
16
+
17
+ // cls.getId() might be undefined during application bootstrap
18
+ // or when the logger is called outside of an HTTP request context.
19
+ if (cls && cls.isActive() && cls.getId()) {
20
+ tracePrefix = this.colorize(`[TraceId-${cls.getId()}]`, logLevel) + ' ';
21
+ }
22
+
23
+ const originalMessage = super.formatMessage(
24
+ logLevel,
25
+ message,
26
+ pidMessage,
27
+ formattedLogLevel,
28
+ contextMessage,
29
+ timestampDiff,
30
+ );
31
+
32
+ return `${tracePrefix}${originalMessage}`;
33
+ }
34
+ }
@@ -0,0 +1,33 @@
1
+ // Domain — pure business primitives, no framework dependencies
2
+ export * from './domain/base/entity.base.js';
3
+ export * from './domain/base/aggregate-root.base.js';
4
+ export * from './domain/base/domain-event.base.js';
5
+ export * from './domain/base/value-object.base.js';
6
+ export * from './domain/exceptions/domain.exception.js';
7
+ export * from './domain/exceptions/error-codes.js';
8
+ export * from './domain/types/pagination.js';
9
+
10
+ // HTTP — web-layer framework glue (filters, interceptors, shared DTOs, logging)
11
+ export * from './http/filters/all-exceptions.filter.js';
12
+ export * from './http/interceptors/logging.interceptor.js';
13
+ export * from './http/interceptors/response.interceptor.js';
14
+ export * from './http/dtos/paginated-response.dto.js';
15
+ export * from './http/dtos/pagination-query.dto.js';
16
+ export * from './http/logging/app.logger.js';
17
+ export * from './http/guards/max-body-size.guard.js';
18
+ export * from './http/decorators/client-info.decorator.js';
19
+ export * from './http/decorators/current-user.decorator.js';
20
+
21
+ // Config — settings read from the environment
22
+ export * from './config/app.config.js';
23
+
24
+ // Utils — stateless pure functions
25
+ export * from './utils/snowflake.util.js';
26
+ export * from './utils/hash.util.js';
27
+ export * from './utils/time.util.js';
28
+ export * from './utils/string.util.js';
29
+ export * from './utils/correlation-id.util.js';
30
+ export * from './utils/ttl.util.js';
31
+
32
+ // Infrastructure — external systems; lines are added by `create-my-stack add <infra>`
33
+ // @infra:barrel
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { Request } from 'express';
3
+ import { resolveCorrelationId } from './correlation-id.util.js';
4
+
5
+ function reqWith(header: string | string[] | undefined): Request {
6
+ return { headers: { 'x-correlation-id': header } } as unknown as Request;
7
+ }
8
+
9
+ describe('resolveCorrelationId', () => {
10
+ it('reuses a well-formed correlation id from the request header', () => {
11
+ const id = resolveCorrelationId(reqWith('abc-123-XYZ'));
12
+ expect(id).toBe('abc-123-XYZ');
13
+ });
14
+
15
+ it('generates a fresh id when the header is missing', () => {
16
+ const id = resolveCorrelationId(reqWith(undefined));
17
+ expect(id).toMatch(/^[0-9a-f-]{36}$/);
18
+ });
19
+
20
+ it('generates a fresh id when the header contains unsafe characters', () => {
21
+ const id = resolveCorrelationId(reqWith('bad\nvalue'));
22
+ expect(id).toMatch(/^[0-9a-f-]{36}$/);
23
+ });
24
+
25
+ it('generates a fresh id when the header is unreasonably long', () => {
26
+ const id = resolveCorrelationId(reqWith('a'.repeat(100)));
27
+ expect(id).toMatch(/^[0-9a-f-]{36}$/);
28
+ });
29
+ });
@@ -0,0 +1,15 @@
1
+ import { randomUUID } from 'crypto';
2
+ import type { Request } from 'express';
3
+
4
+ // Correlation ids are trusted to appear verbatim in logs and error
5
+ // responses, so an incoming x-correlation-id is only reused when it looks
6
+ // like a genuine id (letters/digits/hyphen, bounded length) — anything
7
+ // else (control characters, absurd lengths) falls back to a fresh UUID
8
+ // instead of being logged as-is.
9
+ const CORRELATION_ID_PATTERN = /^[a-zA-Z0-9-]{1,64}$/;
10
+
11
+ export function resolveCorrelationId(req: Request): string {
12
+ const header = req.headers['x-correlation-id'];
13
+ const value = Array.isArray(header) ? header[0] : header;
14
+ return value && CORRELATION_ID_PATTERN.test(value) ? value : randomUUID();
15
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { HashUtil } from './hash.util.js';
3
+
4
+ describe('HashUtil', () => {
5
+ it('should hash and compare passwords correctly', async () => {
6
+ const plainText = 'my-super-secret-password';
7
+ const hash = await HashUtil.hashPassword(plainText);
8
+
9
+ expect(hash).not.toBe(plainText);
10
+
11
+ const isMatch = await HashUtil.comparePassword(plainText, hash);
12
+ expect(isMatch).toBe(true);
13
+
14
+ const isFalseMatch = await HashUtil.comparePassword('wrong-password', hash);
15
+ expect(isFalseMatch).toBe(false);
16
+ });
17
+
18
+ it('should hash with sha256 deterministically', () => {
19
+ // Well-known digest of "abc"
20
+ expect(HashUtil.sha256('abc')).toBe(
21
+ 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
22
+ );
23
+ expect(HashUtil.sha256('abc')).toBe(HashUtil.sha256('abc'));
24
+ expect(HashUtil.sha256('abd')).not.toBe(HashUtil.sha256('abc'));
25
+ });
26
+
27
+ it('should generate random token of given length', () => {
28
+ const token = HashUtil.generateRandomToken(16);
29
+ // 16 bytes = 32 hex characters
30
+ expect(token.length).toBe(32);
31
+ });
32
+
33
+ it('should generate OTP with correct length, digits only', () => {
34
+ for (let i = 0; i < 200; i++) {
35
+ expect(HashUtil.generateOTP(6)).toMatch(/^\d{6}$/);
36
+ }
37
+ });
38
+
39
+ it('should use the whole 10^6 space, leading zeros included', () => {
40
+ // P(no code starts with 0 in 5000 draws) = 0.9^5000, i.e. never
41
+ const codes = Array.from({ length: 5000 }, () => HashUtil.generateOTP(6));
42
+ expect(codes.some((code) => code.startsWith('0'))).toBe(true);
43
+ });
44
+
45
+ it('should draw OTPs from the CSPRNG, never from Math.random', () => {
46
+ const math = vi.spyOn(Math, 'random');
47
+ HashUtil.generateOTP(6);
48
+ expect(math).not.toHaveBeenCalled();
49
+ math.mockRestore();
50
+ });
51
+
52
+ it('should compute a keyed HMAC-SHA256 digest', () => {
53
+ // RFC 4231 test case 2
54
+ expect(HashUtil.hmacSha256('Jefe', 'what do ya want for nothing?')).toBe(
55
+ '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843',
56
+ );
57
+ expect(HashUtil.hmacSha256('key-a', 'value')).not.toBe(
58
+ HashUtil.hmacSha256('key-b', 'value'),
59
+ );
60
+ });
61
+ });