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,101 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { EmailDispatcher, EmailJobHandler, SEND_EMAIL } from './email.job.js';
3
+ import { JobPayloadCipher } from '../messaging/jobs/job-payload-cipher.js';
4
+ import type { IEmailSender } from './email-sender.port.js';
5
+ import type { IJobQueue } from '../messaging/jobs/job-queue.port.js';
6
+
7
+ const MESSAGE = {
8
+ to: 'jane.doe@example.com',
9
+ subject: 'Your code',
10
+ text: 'Code: 482915',
11
+ html: '<p>482915</p>',
12
+ };
13
+
14
+ describe('send-email job', () => {
15
+ let cipher: JobPayloadCipher;
16
+ let jobs: {
17
+ enqueue: ReturnType<typeof vi.fn>;
18
+ register: ReturnType<typeof vi.fn>;
19
+ };
20
+ let sender: { send: ReturnType<typeof vi.fn> };
21
+
22
+ beforeEach(() => {
23
+ cipher = new JobPayloadCipher('s'.repeat(32), 'jobs-email-v1');
24
+ jobs = { enqueue: vi.fn().mockResolvedValue(undefined), register: vi.fn() };
25
+ sender = { send: vi.fn().mockResolvedValue(undefined) };
26
+ });
27
+
28
+ it('retries after 10 s, 30 s and 90 s and runs five at a time', () => {
29
+ expect(SEND_EMAIL.name).toBe('send-email');
30
+ expect(SEND_EMAIL.retryDelaysMs).toEqual([10_000, 30_000, 90_000]);
31
+ expect(SEND_EMAIL.concurrency).toBe(5);
32
+ });
33
+
34
+ describe('EmailDispatcher', () => {
35
+ it('puts the email on the queue as one encrypted payload and nothing else (AC-5)', async () => {
36
+ const dispatcher = new EmailDispatcher(
37
+ jobs as unknown as IJobQueue,
38
+ cipher,
39
+ );
40
+
41
+ await dispatcher.dispatch(MESSAGE);
42
+
43
+ expect(jobs.enqueue).toHaveBeenCalledOnce();
44
+ const [job, data] = jobs.enqueue.mock.calls[0];
45
+ expect(job).toBe(SEND_EMAIL);
46
+ expect(Object.keys(data)).toEqual(['payload']);
47
+ const json = JSON.stringify(data);
48
+ expect(json).not.toContain('jane.doe');
49
+ expect(json).not.toContain('482915');
50
+ expect(cipher.decrypt(data.payload)).toEqual(MESSAGE);
51
+ });
52
+
53
+ it('lets the failure through when the queue cannot take the job', async () => {
54
+ jobs.enqueue.mockRejectedValue(new Error('queue down'));
55
+ const dispatcher = new EmailDispatcher(
56
+ jobs as unknown as IJobQueue,
57
+ cipher,
58
+ );
59
+
60
+ await expect(dispatcher.dispatch(MESSAGE)).rejects.toThrow('queue down');
61
+ });
62
+ });
63
+
64
+ describe('EmailJobHandler', () => {
65
+ const build = () =>
66
+ new EmailJobHandler(
67
+ jobs as unknown as IJobQueue,
68
+ sender as unknown as IEmailSender,
69
+ cipher,
70
+ );
71
+
72
+ it('registers itself for the send-email job when the module starts', () => {
73
+ build().onModuleInit();
74
+
75
+ expect(jobs.register).toHaveBeenCalledWith(
76
+ SEND_EMAIL,
77
+ expect.any(Function),
78
+ );
79
+ });
80
+
81
+ it('decrypts the payload and sends exactly that message (AC-2)', async () => {
82
+ await build().handle({ payload: cipher.encrypt(MESSAGE) });
83
+
84
+ expect(sender.send).toHaveBeenCalledWith(MESSAGE);
85
+ });
86
+
87
+ it('rethrows a failure from the provider so the job is retried (AC-3)', async () => {
88
+ sender.send.mockRejectedValue(new Error('provider down'));
89
+
90
+ await expect(
91
+ build().handle({ payload: cipher.encrypt(MESSAGE) }),
92
+ ).rejects.toThrow('provider down');
93
+ });
94
+
95
+ it('fails, rather than sending something, on a payload it cannot decrypt', async () => {
96
+ await expect(build().handle({ payload: 'garbage' })).rejects.toThrow();
97
+
98
+ expect(sender.send).not.toHaveBeenCalled();
99
+ });
100
+ });
101
+ });
@@ -0,0 +1,64 @@
1
+ import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
2
+ import {
3
+ defineJob,
4
+ JOB_QUEUE,
5
+ type IJobQueue,
6
+ } from '../messaging/jobs/job-queue.port.js';
7
+ import {
8
+ EMAIL_SENDER,
9
+ type EmailMessage,
10
+ type IEmailSender,
11
+ } from './email-sender.port.js';
12
+ import type { IEmailDispatcher } from './email-dispatcher.port.js';
13
+ import { JobPayloadCipher } from '../messaging/jobs/job-payload-cipher.js';
14
+
15
+ /** DI token for the cipher that protects email payloads in the queue. */
16
+ export const EMAIL_PAYLOAD_CIPHER = 'EMAIL_PAYLOAD_CIPHER';
17
+
18
+ /** What travels through the queue: the whole email, encrypted. */
19
+ export interface SendEmailJobData {
20
+ payload: string;
21
+ }
22
+
23
+ /**
24
+ * Sending an email in the background. Retries after 10 s, 30 s and 90 s; after that the
25
+ * job is set aside in the dead-letter queue. Up to 5 emails are sent at the same time.
26
+ */
27
+ export const SEND_EMAIL = defineJob<SendEmailJobData>({
28
+ name: 'send-email',
29
+ retryDelaysMs: [10_000, 30_000, 90_000],
30
+ concurrency: 5,
31
+ });
32
+
33
+ /** Accepts emails by putting them on the job queue. */
34
+ @Injectable()
35
+ export class EmailDispatcher implements IEmailDispatcher {
36
+ constructor(
37
+ @Inject(JOB_QUEUE) private readonly jobs: IJobQueue,
38
+ @Inject(EMAIL_PAYLOAD_CIPHER) private readonly cipher: JobPayloadCipher,
39
+ ) {}
40
+
41
+ dispatch(message: EmailMessage): Promise<void> {
42
+ return this.jobs.enqueue(SEND_EMAIL, {
43
+ payload: this.cipher.encrypt(message),
44
+ });
45
+ }
46
+ }
47
+
48
+ /** Runs the `send-email` jobs: decrypt, then send. A failure is rethrown so the job is retried. */
49
+ @Injectable()
50
+ export class EmailJobHandler implements OnModuleInit {
51
+ constructor(
52
+ @Inject(JOB_QUEUE) private readonly jobs: IJobQueue,
53
+ @Inject(EMAIL_SENDER) private readonly sender: IEmailSender,
54
+ @Inject(EMAIL_PAYLOAD_CIPHER) private readonly cipher: JobPayloadCipher,
55
+ ) {}
56
+
57
+ onModuleInit(): void {
58
+ this.jobs.register(SEND_EMAIL, (data) => this.handle(data));
59
+ }
60
+
61
+ async handle(data: SendEmailJobData): Promise<void> {
62
+ await this.sender.send(this.cipher.decrypt(data.payload));
63
+ }
64
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
+ import { Test } from '@nestjs/testing';
3
+ import { EmailModule } from './email.module.js';
4
+ import { EMAIL_SENDER } from './email-sender.port.js';
5
+ import { EMAIL_DISPATCHER } from './email-dispatcher.port.js';
6
+ import { EmailDispatcher } from './email.job.js';
7
+ import { LogEmailSender } from './log-email-sender.js';
8
+ import { JobQueueModule } from '../messaging/jobs/job-queue.module.js';
9
+
10
+ describe('EmailModule', () => {
11
+ const original = { ...process.env };
12
+ beforeEach(() => {
13
+ process.env.JOB_QUEUE = 'inline';
14
+ process.env.JOB_PAYLOAD_SECRET = 's'.repeat(32);
15
+ });
16
+ afterEach(() => {
17
+ process.env = { ...original };
18
+ });
19
+
20
+ it('provides the adapter chosen by EMAIL_PROVIDER under the IEmailSender token', async () => {
21
+ process.env.EMAIL_PROVIDER = 'log';
22
+
23
+ const moduleRef = await Test.createTestingModule({
24
+ imports: [JobQueueModule, EmailModule],
25
+ }).compile();
26
+
27
+ expect(moduleRef.get(EMAIL_SENDER)).toBeInstanceOf(LogEmailSender);
28
+ });
29
+
30
+ it('provides the dispatcher that other features use to send email', async () => {
31
+ const moduleRef = await Test.createTestingModule({
32
+ imports: [JobQueueModule, EmailModule],
33
+ }).compile();
34
+
35
+ expect(moduleRef.get(EMAIL_DISPATCHER)).toBeInstanceOf(EmailDispatcher);
36
+ });
37
+
38
+ it('sends a dispatched email through the sender, via the queue (inline: right away) (AC-9)', async () => {
39
+ process.env.EMAIL_PROVIDER = 'log';
40
+ const sent: unknown[] = [];
41
+ const moduleRef = await Test.createTestingModule({
42
+ imports: [JobQueueModule, EmailModule],
43
+ })
44
+ .overrideProvider(EMAIL_SENDER)
45
+ .useValue({ send: async (message: unknown) => void sent.push(message) })
46
+ .compile();
47
+ await moduleRef.init();
48
+
49
+ await moduleRef.get(EMAIL_DISPATCHER).dispatch({
50
+ to: 'a@example.com',
51
+ subject: 'Hi',
52
+ text: 'Hello',
53
+ });
54
+
55
+ expect(sent).toEqual([
56
+ { to: 'a@example.com', subject: 'Hi', text: 'Hello' },
57
+ ]);
58
+ await moduleRef.close();
59
+ });
60
+ });
@@ -0,0 +1,34 @@
1
+ import { Global, Module } from '@nestjs/common';
2
+ import { EMAIL_SENDER } from './email-sender.port.js';
3
+ import { EMAIL_DISPATCHER } from './email-dispatcher.port.js';
4
+ import { createEmailSender } from './email-sender.factory.js';
5
+ import { JobPayloadCipher } from '../messaging/jobs/job-payload-cipher.js';
6
+ import {
7
+ EMAIL_PAYLOAD_CIPHER,
8
+ EmailDispatcher,
9
+ EmailJobHandler,
10
+ } from './email.job.js';
11
+
12
+ // Global: any feature (registration now, password reset and order mails later) can
13
+ // inject the dispatcher without importing a feature module.
14
+ @Global()
15
+ @Module({
16
+ providers: [
17
+ {
18
+ provide: EMAIL_SENDER,
19
+ useFactory: () => createEmailSender(process.env),
20
+ },
21
+ {
22
+ provide: EMAIL_PAYLOAD_CIPHER,
23
+ useFactory: () =>
24
+ new JobPayloadCipher(
25
+ process.env.JOB_PAYLOAD_SECRET as string,
26
+ 'jobs-email-v1',
27
+ ),
28
+ },
29
+ { provide: EMAIL_DISPATCHER, useClass: EmailDispatcher },
30
+ EmailJobHandler,
31
+ ],
32
+ exports: [EMAIL_SENDER, EMAIL_DISPATCHER],
33
+ })
34
+ export class EmailModule {}
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { Logger } from '@nestjs/common';
3
+ import { LogEmailSender } from './log-email-sender.js';
4
+
5
+ describe('LogEmailSender', () => {
6
+ afterEach(() => vi.restoreAllMocks());
7
+
8
+ it('prints the whole message (including the one-time code) instead of sending it', async () => {
9
+ const log = vi.spyOn(Logger.prototype, 'log').mockImplementation(() => {});
10
+
11
+ await new LogEmailSender().send({
12
+ to: 'a@example.com',
13
+ subject: 'Your code',
14
+ text: 'Code: 123456',
15
+ });
16
+
17
+ const printed = String(log.mock.calls[0][0]);
18
+ expect(printed).toContain('a@example.com');
19
+ expect(printed).toContain('Your code');
20
+ expect(printed).toContain('123456');
21
+ });
22
+
23
+ it('resolves, so a caller can treat it like any other sender', async () => {
24
+ vi.spyOn(Logger.prototype, 'log').mockImplementation(() => {});
25
+
26
+ await expect(
27
+ new LogEmailSender().send({
28
+ to: 'a@example.com',
29
+ subject: 's',
30
+ text: 't',
31
+ }),
32
+ ).resolves.toBeUndefined();
33
+ });
34
+ });
@@ -0,0 +1,17 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import type { EmailMessage, IEmailSender } from './email-sender.port.js';
3
+
4
+ /**
5
+ * Development adapter: prints the email instead of sending it, so the developer
6
+ * can read the one-time code from the server log. It deliberately logs the body,
7
+ * which is why `validateEnv` refuses it in production.
8
+ */
9
+ export class LogEmailSender implements IEmailSender {
10
+ private readonly logger = new Logger(LogEmailSender.name);
11
+
12
+ async send(message: EmailMessage): Promise<void> {
13
+ this.logger.log(
14
+ `\n--- email (not sent: EMAIL_PROVIDER=log) ---\nTo: ${message.to}\nSubject: ${message.subject}\n\n${message.text}\n--------------------------------------------`,
15
+ );
16
+ }
17
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import nodemailer from 'nodemailer';
3
+ import { SmtpEmailSender } from './smtp-email-sender.js';
4
+
5
+ const OPTIONS = {
6
+ host: 'smtp.example.com',
7
+ port: 587,
8
+ secure: false,
9
+ from: 'Ecom <no-reply@example.com>',
10
+ };
11
+
12
+ // nodemailer's JSON transport builds the real message but "sends" it into a string, so
13
+ // the test sees exactly what would have gone out, without opening a connection.
14
+ const capturing = () => {
15
+ const transporter = nodemailer.createTransport({ jsonTransport: true });
16
+ const sendMail = vi.spyOn(transporter, 'sendMail');
17
+ const lastMessage = async () => {
18
+ const info = await sendMail.mock.results[0].value;
19
+ return JSON.parse(info.message);
20
+ };
21
+ return { transporter, sendMail, lastMessage };
22
+ };
23
+
24
+ describe('SmtpEmailSender', () => {
25
+ it('sends from the configured address to the recipient with subject, text and HTML', async () => {
26
+ const { transporter, lastMessage } = capturing();
27
+
28
+ await new SmtpEmailSender(OPTIONS, transporter).send({
29
+ to: 'a@example.com',
30
+ subject: 'Hello',
31
+ text: 'plain body',
32
+ html: '<p>html body</p>',
33
+ });
34
+
35
+ const sent = await lastMessage();
36
+ expect(sent.from.address).toBe('no-reply@example.com');
37
+ expect(sent.from.name).toBe('Ecom');
38
+ expect(sent.to[0].address).toBe('a@example.com');
39
+ expect(sent.subject).toBe('Hello');
40
+ expect(sent.text).toBe('plain body');
41
+ expect(sent.html).toBe('<p>html body</p>');
42
+ });
43
+
44
+ it('sends a text-only message when there is no HTML', async () => {
45
+ const { transporter, lastMessage } = capturing();
46
+
47
+ await new SmtpEmailSender(OPTIONS, transporter).send({
48
+ to: 'a@example.com',
49
+ subject: 'Hello',
50
+ text: 'plain body',
51
+ });
52
+
53
+ const sent = await lastMessage();
54
+ expect(sent.text).toBe('plain body');
55
+ expect(sent.html).toBeUndefined();
56
+ });
57
+
58
+ it('rejects when the server refuses, so the caller can decide what that means', async () => {
59
+ const { transporter, sendMail } = capturing();
60
+ sendMail.mockRejectedValue(new Error('550 mailbox unavailable'));
61
+
62
+ await expect(
63
+ new SmtpEmailSender(OPTIONS, transporter).send({
64
+ to: 'a@example.com',
65
+ subject: 'Hello',
66
+ text: 'plain body',
67
+ }),
68
+ ).rejects.toThrow('550 mailbox unavailable');
69
+ });
70
+
71
+ it('can be built from options alone, without opening a connection', () => {
72
+ expect(
73
+ () => new SmtpEmailSender({ ...OPTIONS, user: 'apikey', pass: 'secret' }),
74
+ ).not.toThrow();
75
+ });
76
+
77
+ describe('transport security', () => {
78
+ const transportOptionsFor = (options: object) => {
79
+ const create = vi.spyOn(nodemailer, 'createTransport');
80
+ new SmtpEmailSender({ ...OPTIONS, ...options });
81
+ const passed = create.mock.calls[0][0] as { requireTLS?: boolean };
82
+ create.mockRestore();
83
+ return passed;
84
+ };
85
+
86
+ it('refuses to continue unencrypted when credentials go over a plain port', () => {
87
+ expect(transportOptionsFor({ user: 'u', pass: 'p' }).requireTLS).toBe(
88
+ true,
89
+ );
90
+ });
91
+
92
+ it('does not insist on TLS for an implicit-TLS port or a local relay without credentials', () => {
93
+ expect(
94
+ transportOptionsFor({ secure: true, user: 'u', pass: 'p' }).requireTLS,
95
+ ).toBe(false);
96
+ expect(transportOptionsFor({}).requireTLS).toBe(false);
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,63 @@
1
+ import nodemailer from 'nodemailer';
2
+ import type { Transporter } from 'nodemailer';
3
+ import type { EmailMessage, IEmailSender } from './email-sender.port.js';
4
+
5
+ export interface SmtpOptions {
6
+ host: string;
7
+ port: number;
8
+ /** TLS from the first byte (port 465). With `false` the connection is upgraded with STARTTLS when the server offers it (port 587). */
9
+ secure: boolean;
10
+ /** Both or neither: some relays accept mail from a trusted network without logging in. */
11
+ user?: string;
12
+ pass?: string;
13
+ /** The From header, e.g. `Ecom <no-reply@example.com>`. */
14
+ from: string;
15
+ }
16
+
17
+ // A provider that hangs must not hang the request that is waiting on it
18
+ const CONNECTION_TIMEOUT_MS = 10_000;
19
+ const GREETING_TIMEOUT_MS = 10_000;
20
+ const SOCKET_TIMEOUT_MS = 20_000;
21
+
22
+ /**
23
+ * Sends email through any SMTP server (a hosted provider, Gmail's SMTP, a local relay...).
24
+ * The message goes out as multipart text + HTML.
25
+ */
26
+ export class SmtpEmailSender implements IEmailSender {
27
+ private readonly transporter: Transporter;
28
+
29
+ constructor(
30
+ private readonly options: SmtpOptions,
31
+ // Injectable so tests can capture the message instead of opening a connection
32
+ transporter?: Transporter,
33
+ ) {
34
+ this.transporter =
35
+ transporter ??
36
+ nodemailer.createTransport({
37
+ host: options.host,
38
+ port: options.port,
39
+ secure: options.secure,
40
+ // With credentials on a plain port, refuse to continue unencrypted if the server (or someone
41
+ // in between) drops STARTTLS, rather than send the password and codes in clear text.
42
+ // A local relay without credentials (e.g. a dev mail catcher) keeps working without TLS.
43
+ requireTLS: !options.secure && options.user !== undefined,
44
+ auth:
45
+ options.user !== undefined
46
+ ? { user: options.user, pass: options.pass }
47
+ : undefined,
48
+ connectionTimeout: CONNECTION_TIMEOUT_MS,
49
+ greetingTimeout: GREETING_TIMEOUT_MS,
50
+ socketTimeout: SOCKET_TIMEOUT_MS,
51
+ });
52
+ }
53
+
54
+ async send(message: EmailMessage): Promise<void> {
55
+ await this.transporter.sendMail({
56
+ from: this.options.from,
57
+ to: message.to,
58
+ subject: message.subject,
59
+ text: message.text,
60
+ html: message.html,
61
+ });
62
+ }
63
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "id": "email",
3
+ "label": "Email (SMTP / log adapter, sent through the job queue)",
4
+ "templates": ["nest-ddd"],
5
+ "requires": ["rabbitmq"],
6
+ "dependencies": {
7
+ "nodemailer": "^10.0.13"
8
+ },
9
+ "devDependencies": {
10
+ "@types/nodemailer": "^8.0.2"
11
+ },
12
+ "env": [
13
+ "# Email — 'log' only prints messages (dev); 'smtp' sends through any SMTP server",
14
+ "EMAIL_PROVIDER=log",
15
+ "SMTP_HOST=",
16
+ "SMTP_PORT=587",
17
+ "SMTP_USER=",
18
+ "SMTP_PASS=",
19
+ "EMAIL_FROM=\"{{PROJECT_NAME}} <no-reply@example.com>\"",
20
+ "# Encrypts job payloads (email addresses, one-time codes) in the queue. >= 32 chars",
21
+ "JOB_PAYLOAD_SECRET=change-me-to-a-random-string-of-32-chars-or-more"
22
+ ],
23
+ "inject": [
24
+ {
25
+ "file": "src/app.module.ts",
26
+ "marker": "// @infra:imports",
27
+ "lines": ["import { EmailModule } from './core/infrastructure/email/email.module.js';"]
28
+ },
29
+ {
30
+ "file": "src/app.module.ts",
31
+ "marker": "// @infra:modules",
32
+ "lines": ["EmailModule,"]
33
+ },
34
+ {
35
+ "file": "src/core/index.ts",
36
+ "marker": "// @infra:barrel",
37
+ "lines": [
38
+ "export * from './infrastructure/email/email-sender.port.js';",
39
+ "export * from './infrastructure/email/email-dispatcher.port.js';",
40
+ "export * from './infrastructure/email/email-layout.js';",
41
+ "export * from './infrastructure/email/email-sender.factory.js';",
42
+ "export * from './infrastructure/email/log-email-sender.js';",
43
+ "export * from './infrastructure/email/smtp-email-sender.js';",
44
+ "export * from './infrastructure/email/email.module.js';"
45
+ ]
46
+ },
47
+ {
48
+ "file": "src/core/config/env.validation.ts",
49
+ "marker": "// @infra:env-imports",
50
+ "lines": ["import { validateEmailEnv } from './email.env.js';"]
51
+ },
52
+ {
53
+ "file": "src/core/config/env.validation.ts",
54
+ "marker": "// @infra:env-checks",
55
+ "lines": ["validateEmailEnv(config, errors);"]
56
+ }
57
+ ]
58
+ }
@@ -0,0 +1,12 @@
1
+ postgres:
2
+ image: postgres:15-alpine
3
+ container_name: {{PROJECT_NAME_SNAKE}}_postgres
4
+ environment:
5
+ POSTGRES_USER: root
6
+ POSTGRES_PASSWORD: rootpassword
7
+ POSTGRES_DB: {{PROJECT_NAME_SNAKE}}_db
8
+ ports:
9
+ - '5432:5432'
10
+ volumes:
11
+ - postgres_data:/var/lib/postgresql/data
12
+ restart: unless-stopped
@@ -0,0 +1,10 @@
1
+ // Prisma schema. Models are added here; migrations live in prisma/migrations.
2
+ // Always use `pnpm prisma:migrate` (never `prisma db push`) so every change leaves a migration file.
3
+
4
+ generator client {
5
+ provider = "prisma-client-js"
6
+ }
7
+
8
+ datasource db {
9
+ provider = "postgresql"
10
+ }
@@ -0,0 +1,10 @@
1
+ import 'dotenv/config';
2
+ import { defineConfig } from 'prisma/config';
3
+
4
+ export default defineConfig({
5
+ schema: 'prisma/schema.prisma',
6
+ migrations: { path: 'prisma/migrations' },
7
+ datasource: {
8
+ url: process.env.DATABASE_URL,
9
+ },
10
+ });
@@ -0,0 +1,13 @@
1
+ /** DATABASE_URL must be a postgres:// connection string. */
2
+ export function validateDatabaseEnv(
3
+ config: Record<string, unknown>,
4
+ errors: string[],
5
+ ): void {
6
+ const databaseUrl = config.DATABASE_URL;
7
+ if (
8
+ typeof databaseUrl !== 'string' ||
9
+ !/^postgres(ql)?:\/\/.+/.test(databaseUrl)
10
+ ) {
11
+ errors.push('DATABASE_URL must be set to a postgres:// connection string');
12
+ }
13
+ }
@@ -0,0 +1,9 @@
1
+ import { Global, Module } from '@nestjs/common';
2
+ import { PrismaService } from './prisma.service.js';
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [PrismaService],
7
+ exports: [PrismaService],
8
+ })
9
+ export class DatabaseModule {}
@@ -0,0 +1,26 @@
1
+ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ import pg from 'pg';
5
+ import { PrismaPg } from '@prisma/adapter-pg';
6
+
7
+ @Injectable()
8
+ export class PrismaService
9
+ extends PrismaClient
10
+ implements OnModuleInit, OnModuleDestroy
11
+ {
12
+ constructor() {
13
+ const connectionString = process.env.DATABASE_URL;
14
+ const pool = new pg.Pool({ connectionString });
15
+ const adapter = new PrismaPg(pool);
16
+ super({ adapter });
17
+ }
18
+
19
+ async onModuleInit() {
20
+ await this.$connect();
21
+ }
22
+
23
+ async onModuleDestroy() {
24
+ await this.$disconnect();
25
+ }
26
+ }