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,196 @@
1
+ import { Injectable, OnModuleDestroy, Logger } from '@nestjs/common';
2
+ import { Redis } from 'ioredis';
3
+
4
+ type TtlValue = number;
5
+
6
+ @Injectable()
7
+ export class RedisService implements OnModuleDestroy {
8
+ private readonly logger = new Logger(RedisService.name);
9
+ private readonly redisClient: Redis;
10
+ private inFlightRequests = new Map<string, Promise<any>>();
11
+ private readonly NULLABLE_SENTINEL = '__NULL_CACHE_SENTINEL__';
12
+
13
+ constructor() {
14
+ this.redisClient = new Redis({
15
+ host: process.env.REDIS_HOST || 'localhost',
16
+ port: Number(process.env.REDIS_PORT) || 6379,
17
+ db: Number(process.env.REDIS_DB) || 0,
18
+ connectTimeout: 5000,
19
+ // Without this, a command on an already-open-but-unresponsive
20
+ // connection (Redis wedged, network black hole, ...) hangs forever
21
+ // instead of rejecting — every caller here awaits these commands
22
+ // directly, so that would hang the HTTP request itself.
23
+ commandTimeout: 5000,
24
+ });
25
+ }
26
+
27
+ getClient(): Redis {
28
+ return this.redisClient;
29
+ }
30
+
31
+ async exists(key: string): Promise<boolean> {
32
+ try {
33
+ return (await this.redisClient.exists(key)) === 1;
34
+ } catch (error: any) {
35
+ this.logger.error(`Redis exists error: ${error.message}`);
36
+ return false;
37
+ }
38
+ }
39
+
40
+ // Health-check liveness
41
+ async ping(): Promise<boolean> {
42
+ try {
43
+ return (await this.redisClient.ping()) === 'PONG';
44
+ } catch (error: any) {
45
+ this.logger.error(`Redis ping error: ${error.message}`);
46
+ return false;
47
+ }
48
+ }
49
+
50
+ // Utils
51
+ private withJitter(ttl: number, percent = 0.1): number {
52
+ const delta = ttl * percent;
53
+ const jitter = Math.floor(Math.random() * delta);
54
+ return ttl + jitter;
55
+ }
56
+
57
+ private serialize(value: any): string {
58
+ return JSON.stringify(value);
59
+ }
60
+
61
+ private deserialize<T>(value: string | null): T | null {
62
+ if (!value) return null;
63
+ try {
64
+ return JSON.parse(value) as T;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ // Base cache
71
+ async get<T>(key: string): Promise<T | null> {
72
+ try {
73
+ const data = await this.redisClient.get(key);
74
+ return this.deserialize<T>(data);
75
+ } catch (error: any) {
76
+ this.logger.error(`Redis get error: ${error.message}`);
77
+ return null;
78
+ }
79
+ }
80
+
81
+ async set(key: string, value: any, ttl?: TtlValue): Promise<void> {
82
+ try {
83
+ if (ttl) {
84
+ const finalTtl = this.withJitter(ttl);
85
+ await this.redisClient.set(key, this.serialize(value), 'EX', finalTtl);
86
+ } else {
87
+ await this.redisClient.set(key, this.serialize(value));
88
+ }
89
+ } catch (error: any) {
90
+ this.logger.error(`Redis set error: ${error.message}`);
91
+ }
92
+ }
93
+
94
+ async del(key: string): Promise<void> {
95
+ try {
96
+ await this.redisClient.del(key);
97
+ } catch (error: any) {
98
+ this.logger.error(`Redis del error: ${error.message}`);
99
+ }
100
+ }
101
+
102
+ // Tracker (version)
103
+ async getVersion(trackerKey: string): Promise<number> {
104
+ const version = await this.redisClient.get(trackerKey);
105
+ return version ? Number(version) : 0;
106
+ }
107
+
108
+ async bumpVersion(trackerKey: string): Promise<number> {
109
+ return this.redisClient.incr(trackerKey);
110
+ }
111
+
112
+ // Detail cache (Singleflight)
113
+ async getOrSetDetail<T>(
114
+ key: string,
115
+ ttl: TtlValue,
116
+ fetcher: () => Promise<T>,
117
+ ): Promise<T> {
118
+ // 1. Check cache
119
+ const cached = await this.get<T>(key);
120
+ if (cached) return cached;
121
+
122
+ // 2. Singleflight
123
+ if (this.inFlightRequests.has(key)) {
124
+ this.logger.debug(
125
+ `Singleflight: Waiting for in-flight request for key: ${key}`,
126
+ );
127
+ return this.inFlightRequests.get(key);
128
+ }
129
+
130
+ // 3. Perform fetch
131
+ const fetchPromise = fetcher()
132
+ .then(async (data) => {
133
+ if (data) {
134
+ await this.set(key, data, ttl);
135
+ }
136
+ this.inFlightRequests.delete(key);
137
+ return data;
138
+ })
139
+ .catch((err) => {
140
+ this.inFlightRequests.delete(key);
141
+ throw err;
142
+ });
143
+
144
+ this.inFlightRequests.set(key, fetchPromise);
145
+ return fetchPromise;
146
+ }
147
+
148
+ // Nullable Cache (cache cả null để chống cache penetration)
149
+ async getOrSetNullableDetail<T>(
150
+ key: string,
151
+ ttl: TtlValue,
152
+ fetcher: () => Promise<T | null>,
153
+ ): Promise<T | null> {
154
+ const cachedStr = await this.redisClient.get(key);
155
+ if (cachedStr === this.NULLABLE_SENTINEL) return null;
156
+
157
+ const cached = this.deserialize<T>(cachedStr);
158
+ if (cached) return cached;
159
+
160
+ if (this.inFlightRequests.has(key)) {
161
+ return this.inFlightRequests.get(key);
162
+ }
163
+
164
+ const fetchPromise = fetcher()
165
+ .then(async (data) => {
166
+ if (data !== null && data !== undefined) {
167
+ await this.set(key, data, ttl);
168
+ } else {
169
+ const finalTtl = this.withJitter(ttl);
170
+ await this.redisClient.set(
171
+ key,
172
+ this.NULLABLE_SENTINEL,
173
+ 'EX',
174
+ finalTtl,
175
+ );
176
+ }
177
+ this.inFlightRequests.delete(key);
178
+ return data;
179
+ })
180
+ .catch((err) => {
181
+ this.inFlightRequests.delete(key);
182
+ throw err;
183
+ });
184
+
185
+ this.inFlightRequests.set(key, fetchPromise);
186
+ return fetchPromise;
187
+ }
188
+
189
+ async invalidateDetail(key: string) {
190
+ await this.del(key);
191
+ }
192
+
193
+ async onModuleDestroy() {
194
+ await this.redisClient.quit();
195
+ }
196
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "id": "redis",
3
+ "label": "Redis (cache, distributed lock, rate-limit, idempotency)",
4
+ "default": true,
5
+ "templates": ["nest-ddd"],
6
+ "dependencies": {
7
+ "ioredis": "^6.0.0"
8
+ },
9
+ "env": [
10
+ "# Redis (docker-compose.yml)",
11
+ "REDIS_HOST=localhost",
12
+ "REDIS_PORT=6379",
13
+ "REDIS_DB=0"
14
+ ],
15
+ "docker": {
16
+ "file": "docker.service.yml",
17
+ "volumes": ["redis_data"]
18
+ },
19
+ "inject": [
20
+ {
21
+ "file": "src/app.module.ts",
22
+ "marker": "// @infra:imports",
23
+ "lines": [
24
+ "import { RedisModule } from './core/infrastructure/redis/redis.module.js';",
25
+ "import { IdempotencyModule } from './core/infrastructure/redis/idempotency/idempotency.module.js';",
26
+ "import { RateLimitModule } from './core/infrastructure/redis/rate-limit/rate-limit.module.js';",
27
+ "import { LockModule } from './core/infrastructure/redis/lock/lock.module.js';"
28
+ ]
29
+ },
30
+ {
31
+ "file": "src/app.module.ts",
32
+ "marker": "// @infra:modules",
33
+ "lines": ["RedisModule,", "IdempotencyModule,", "RateLimitModule,", "LockModule,"]
34
+ },
35
+ {
36
+ "file": "src/core/index.ts",
37
+ "marker": "// @infra:barrel",
38
+ "lines": [
39
+ "export * from './infrastructure/redis/cache.constants.js';",
40
+ "export * from './infrastructure/redis/redis.service.js';",
41
+ "export * from './infrastructure/redis/redis.module.js';",
42
+ "export * from './infrastructure/redis/idempotency/idempotency.service.js';",
43
+ "export * from './infrastructure/redis/idempotency/idempotency.module.js';",
44
+ "export * from './infrastructure/redis/rate-limit/rate-limit.service.js';",
45
+ "export * from './infrastructure/redis/rate-limit/rate-limit.module.js';",
46
+ "export * from './infrastructure/redis/lock/lock.service.js';",
47
+ "export * from './infrastructure/redis/lock/lock.module.js';",
48
+ "export * from './http/interceptors/rate-limit.interceptor.js';",
49
+ "export * from './http/decorators/rate-limit.decorator.js';"
50
+ ]
51
+ }
52
+ ]
53
+ }
@@ -0,0 +1,43 @@
1
+ // Cloudinary is optional, but its three settings go together
2
+ const CLOUDINARY_VARS = [
3
+ 'CLOUDINARY_CLOUD_NAME',
4
+ 'CLOUDINARY_API_KEY',
5
+ 'CLOUDINARY_API_SECRET',
6
+ ];
7
+ const CLOUDINARY_CLOUD_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
8
+ // A folder path such as app/avatars: letters, digits, _ and -, parts separated by one slash
9
+ const CLOUDINARY_FOLDER_PATTERN = /^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$/;
10
+
11
+ const present = (config: Record<string, unknown>, name: string) =>
12
+ typeof config[name] === 'string' && (config[name] as string).trim() !== '';
13
+
14
+ /** Image storage is optional; when any setting is given, all three are needed and must look right. */
15
+ export function validateStorageEnv(
16
+ config: Record<string, unknown>,
17
+ errors: string[],
18
+ ): void {
19
+ const given = CLOUDINARY_VARS.filter((name) => present(config, name));
20
+
21
+ if (given.length > 0 && given.length < CLOUDINARY_VARS.length) {
22
+ const missing = CLOUDINARY_VARS.filter((name) => !present(config, name));
23
+ errors.push(
24
+ `${CLOUDINARY_VARS.join(', ')} must all be set together, or none of them (missing: ${missing.join(', ')})`,
25
+ );
26
+ } else if (
27
+ present(config, 'CLOUDINARY_CLOUD_NAME') &&
28
+ !CLOUDINARY_CLOUD_NAME_PATTERN.test(
29
+ (config.CLOUDINARY_CLOUD_NAME as string).trim(),
30
+ )
31
+ ) {
32
+ errors.push('CLOUDINARY_CLOUD_NAME may only contain letters, digits, _ and -');
33
+ }
34
+
35
+ if (
36
+ present(config, 'CLOUDINARY_FOLDER') &&
37
+ !CLOUDINARY_FOLDER_PATTERN.test((config.CLOUDINARY_FOLDER as string).trim())
38
+ ) {
39
+ errors.push(
40
+ 'CLOUDINARY_FOLDER must look like app/avatars: letters, digits, _ and -, separated by single slashes',
41
+ );
42
+ }
43
+ }
@@ -0,0 +1,275 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Logger } from '@nestjs/common';
3
+ import {
4
+ CloudinaryImageStorage,
5
+ type CloudinaryClient,
6
+ } from './cloudinary-image-storage.js';
7
+ import { UnconfiguredImageStorage } from './unconfigured-image-storage.js';
8
+ import {
9
+ cloudinarySettingsFromEnv,
10
+ createImageStorage,
11
+ } from './image-storage.factory.js';
12
+ import type { ImageFit, ImageType } from './image-storage.port.js';
13
+
14
+ const SETTINGS = {
15
+ cloudName: 'demo',
16
+ apiKey: '123456',
17
+ apiSecret: 'super-secret-value',
18
+ folder: 'ecom',
19
+ };
20
+ const GOOD_ADDRESS =
21
+ 'https://res.cloudinary.com/demo/image/upload/v1/ecom/avatars/u-1.png';
22
+ const AVATAR_FIT: ImageFit = {
23
+ width: 256,
24
+ height: 256,
25
+ crop: 'fill',
26
+ focus: 'face',
27
+ };
28
+ const code = (promise: Promise<unknown>) =>
29
+ promise.then(
30
+ () => 'resolved',
31
+ (e: any) => e.code ?? e.constructor.name,
32
+ );
33
+
34
+ describe('CloudinaryImageStorage', () => {
35
+ let client: {
36
+ upload: ReturnType<typeof vi.fn>;
37
+ destroy: ReturnType<typeof vi.fn>;
38
+ };
39
+ let logged: string[];
40
+ const storage = () =>
41
+ new CloudinaryImageStorage(SETTINGS, client as unknown as CloudinaryClient);
42
+ const upload = (
43
+ options: {
44
+ type?: ImageType;
45
+ path?: string;
46
+ fit?: ImageFit;
47
+ } = {},
48
+ ) =>
49
+ storage().upload({
50
+ path: options.path ?? 'avatars/u-1',
51
+ image: Buffer.from('FILE-CONTENT'),
52
+ type: options.type ?? 'png',
53
+ fit: options.fit ?? AVATAR_FIT,
54
+ });
55
+
56
+ beforeEach(() => {
57
+ vi.restoreAllMocks();
58
+ logged = [];
59
+ vi.spyOn(Logger.prototype, 'error').mockImplementation((m: any) => {
60
+ logged.push(String(m));
61
+ });
62
+ client = {
63
+ upload: vi.fn().mockResolvedValue({ secure_url: GOOD_ADDRESS }),
64
+ destroy: vi.fn().mockResolvedValue({ result: 'ok' }),
65
+ };
66
+ });
67
+
68
+ it('stores the picture under the app folder at the given path, replacing what was there', async () => {
69
+ expect(await upload()).toBe(GOOD_ADDRESS);
70
+
71
+ const [image, options] = client.upload.mock.calls[0];
72
+ expect(image.toString()).toBe('FILE-CONTENT');
73
+ expect(options).toMatchObject({
74
+ public_id: 'ecom/avatars/u-1',
75
+ overwrite: true,
76
+ invalidate: true,
77
+ allowed_formats: ['png'],
78
+ });
79
+ expect(options.timeout).toBeGreaterThan(0);
80
+ });
81
+
82
+ it('cuts to a square around the face when asked to', async () => {
83
+ await upload();
84
+
85
+ expect(client.upload.mock.calls[0][1].transformation).toEqual([
86
+ {
87
+ width: 256,
88
+ height: 256,
89
+ crop: 'fill',
90
+ gravity: 'face',
91
+ quality: 'auto',
92
+ },
93
+ ]);
94
+ });
95
+
96
+ it('shrinks to fit without enlarging, or fills from the centre, when asked to', async () => {
97
+ await upload({ fit: { width: 800, height: 800, crop: 'limit' } });
98
+ await upload({
99
+ fit: { width: 100, height: 50, crop: 'fill', focus: 'center' },
100
+ });
101
+
102
+ const [limit, center] = client.upload.mock.calls.map(
103
+ (call) => call[1].transformation[0],
104
+ );
105
+ expect(limit).toEqual({
106
+ width: 800,
107
+ height: 800,
108
+ crop: 'limit',
109
+ quality: 'auto',
110
+ });
111
+ expect(center).not.toHaveProperty('gravity');
112
+ expect(center).toMatchObject({ crop: 'fill', width: 100, height: 50 });
113
+ });
114
+
115
+ it('names a JPEG the way Cloudinary does', async () => {
116
+ await upload({ type: 'jpeg' });
117
+
118
+ expect(client.upload.mock.calls[0][1].allowed_formats).toEqual(['jpg']);
119
+ });
120
+
121
+ it.each([
122
+ ['another cloud', 'https://res.cloudinary.com/other/image/upload/a.png'],
123
+ [
124
+ 'a plain http address',
125
+ 'http://res.cloudinary.com/demo/image/upload/a.png',
126
+ ],
127
+ ['another host', 'https://evil.example.com/demo/a.png'],
128
+ ['a look-alike host', 'https://res.cloudinary.com.evil.example/demo/a.png'],
129
+ ['nothing', undefined],
130
+ ])('does not trust an answer pointing at %s', async (_name, url) => {
131
+ client.upload.mockResolvedValue({ secure_url: url });
132
+
133
+ expect(await code(upload())).toBe('STO-002');
134
+ });
135
+
136
+ it('turns "Cloudinary cannot read this as an image" (400) into STO-003', async () => {
137
+ client.upload.mockRejectedValue({
138
+ message: 'Invalid image file',
139
+ http_code: 400,
140
+ });
141
+
142
+ expect(await code(upload())).toBe('STO-003');
143
+ });
144
+
145
+ it.each([
146
+ ['a network failure', new Error('socket hang up')],
147
+ ['a timeout', { message: 'Request Timeout', http_code: 499 }],
148
+ ['wrong keys', { message: 'Invalid Signature', http_code: 401 }],
149
+ ['no reason at all', undefined],
150
+ ])('answers STO-002 for %s', async (_name, failure) => {
151
+ client.upload.mockRejectedValue(failure);
152
+
153
+ expect(await code(upload())).toBe('STO-002');
154
+ });
155
+
156
+ it('logs only the reason of a failure, not the keys or the file', async () => {
157
+ client.upload.mockRejectedValue(new Error('socket hang up'));
158
+
159
+ await upload().catch(() => undefined);
160
+
161
+ const everything = logged.join('\n');
162
+ expect(everything).toContain('socket hang up');
163
+ for (const secret of [
164
+ SETTINGS.apiSecret,
165
+ SETTINGS.apiKey,
166
+ 'FILE-CONTENT',
167
+ ]) {
168
+ expect(everything).not.toContain(secret);
169
+ }
170
+ });
171
+
172
+ it('leaves a trace in the log when Cloudinary calls the file unreadable, since our own options can cause that too', async () => {
173
+ client.upload.mockRejectedValue({
174
+ message: 'Invalid transformation',
175
+ http_code: 400,
176
+ });
177
+ const warnings: string[] = [];
178
+ vi.spyOn(Logger.prototype, 'warn').mockImplementation((m: any) => {
179
+ warnings.push(String(m));
180
+ });
181
+
182
+ await upload().catch(() => undefined);
183
+
184
+ expect(warnings.join(String.fromCharCode(10))).toContain(
185
+ 'Invalid transformation',
186
+ );
187
+ expect(warnings.join(String.fromCharCode(10))).not.toContain(
188
+ 'FILE-CONTENT',
189
+ );
190
+ });
191
+
192
+ it.each([
193
+ '',
194
+ '/avatars/u-1',
195
+ 'avatars/',
196
+ 'avatars//u-1',
197
+ '../secret',
198
+ 'avatars/u 1',
199
+ 'avatars/u-1.png',
200
+ ])('refuses the path %j, which is a bug in the caller', async (path) => {
201
+ await expect(upload({ path })).rejects.toThrow(/Invalid image path/);
202
+ await expect(storage().delete(path)).rejects.toThrow(/Invalid image path/);
203
+ expect(client.upload).not.toHaveBeenCalled();
204
+ expect(client.destroy).not.toHaveBeenCalled();
205
+ });
206
+
207
+ it('removes the picture at the same place and clears the cache', async () => {
208
+ await storage().delete('avatars/u-1');
209
+
210
+ expect(client.destroy).toHaveBeenCalledWith('ecom/avatars/u-1', {
211
+ invalidate: true,
212
+ timeout: expect.any(Number),
213
+ });
214
+ });
215
+
216
+ it('answers STO-002 when removing fails', async () => {
217
+ client.destroy.mockRejectedValue(new Error('down'));
218
+
219
+ expect(await code(storage().delete('avatars/u-1'))).toBe('STO-002');
220
+ });
221
+ });
222
+
223
+ describe('UnconfiguredImageStorage', () => {
224
+ it('refuses to upload with STO-001 and has nothing to remove, without an error', async () => {
225
+ const storage = new UnconfiguredImageStorage();
226
+
227
+ expect(
228
+ await code(
229
+ storage.upload({
230
+ path: 'avatars/u-1',
231
+ image: Buffer.from('x'),
232
+ type: 'png',
233
+ fit: AVATAR_FIT,
234
+ }),
235
+ ),
236
+ ).toBe('STO-001');
237
+ await expect(storage.delete('avatars/u-1')).resolves.toBeUndefined();
238
+ });
239
+ });
240
+
241
+ describe('image storage choice', () => {
242
+ const ENV = {
243
+ CLOUDINARY_CLOUD_NAME: ' demo ',
244
+ CLOUDINARY_API_KEY: '123456',
245
+ CLOUDINARY_API_SECRET: 'super-secret-value',
246
+ };
247
+
248
+ it('uses Cloudinary when all three settings are present, with the default folder', () => {
249
+ expect(cloudinarySettingsFromEnv(ENV)).toEqual({
250
+ cloudName: 'demo',
251
+ apiKey: '123456',
252
+ apiSecret: 'super-secret-value',
253
+ folder: '{{PROJECT_NAME}}',
254
+ });
255
+ expect(createImageStorage(ENV)).toBeInstanceOf(CloudinaryImageStorage);
256
+ });
257
+
258
+ it('takes the folder from CLOUDINARY_FOLDER', () => {
259
+ const settings = cloudinarySettingsFromEnv({
260
+ ...ENV,
261
+ CLOUDINARY_FOLDER: 'shop/dev',
262
+ });
263
+
264
+ expect(settings?.folder).toBe('shop/dev');
265
+ });
266
+
267
+ it.each([
268
+ {},
269
+ { CLOUDINARY_CLOUD_NAME: 'demo' },
270
+ { ...ENV, CLOUDINARY_API_SECRET: ' ' },
271
+ ])('falls back to "not configured" for %j', (env) => {
272
+ expect(cloudinarySettingsFromEnv(env)).toBeNull();
273
+ expect(createImageStorage(env)).toBeInstanceOf(UnconfiguredImageStorage);
274
+ });
275
+ });