@venizia/ignis-docs 0.0.8 → 0.2.0

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 (180) hide show
  1. package/README.md +7 -7
  2. package/content/best-practices/api-usage-examples.md +15 -12
  3. package/content/best-practices/architectural-patterns.md +70 -78
  4. package/content/best-practices/architecture-decisions.md +91 -60
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
  6. package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
  7. package/content/best-practices/code-style-standards/control-flow.md +5 -2
  8. package/content/best-practices/code-style-standards/documentation.md +13 -13
  9. package/content/best-practices/code-style-standards/function-patterns.md +9 -10
  10. package/content/best-practices/code-style-standards/index.md +1 -1
  11. package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
  12. package/content/best-practices/code-style-standards/route-definitions.md +30 -12
  13. package/content/best-practices/code-style-standards/tooling.md +8 -5
  14. package/content/best-practices/code-style-standards/type-safety.md +13 -12
  15. package/content/best-practices/common-pitfalls.md +56 -37
  16. package/content/best-practices/contribution-workflow.md +13 -14
  17. package/content/best-practices/data-modeling.md +46 -22
  18. package/content/best-practices/deployment-strategies.md +28 -27
  19. package/content/best-practices/error-handling.md +48 -24
  20. package/content/best-practices/index.md +5 -5
  21. package/content/best-practices/performance-optimization.md +40 -31
  22. package/content/best-practices/security-guidelines.md +52 -23
  23. package/content/best-practices/testing-strategies.md +65 -51
  24. package/content/best-practices/troubleshooting-tips.md +24 -24
  25. package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
  26. package/content/extensions/components/authentication/api.md +19 -19
  27. package/content/extensions/components/authentication/errors.md +7 -7
  28. package/content/extensions/components/authentication/index.md +10 -8
  29. package/content/extensions/components/authentication/usage.md +101 -6
  30. package/content/extensions/components/authorization/api.md +45 -25
  31. package/content/extensions/components/authorization/errors.md +6 -6
  32. package/content/extensions/components/authorization/index.md +11 -10
  33. package/content/extensions/components/authorization/usage.md +21 -21
  34. package/content/extensions/components/health-check.md +1 -1
  35. package/content/extensions/components/index.md +5 -5
  36. package/content/extensions/components/mail/errors.md +15 -15
  37. package/content/extensions/components/mail/index.md +1 -2
  38. package/content/extensions/components/mail/usage.md +1 -1
  39. package/content/extensions/components/request-tracker.md +1 -1
  40. package/content/extensions/components/socket-io/api.md +9 -9
  41. package/content/extensions/components/socket-io/errors.md +5 -5
  42. package/content/extensions/components/socket-io/index.md +8 -8
  43. package/content/extensions/components/socket-io/usage.md +1 -1
  44. package/content/extensions/components/static-asset/api.md +17 -4
  45. package/content/extensions/components/static-asset/errors.md +4 -4
  46. package/content/extensions/components/static-asset/index.md +26 -28
  47. package/content/extensions/components/static-asset/usage.md +13 -12
  48. package/content/extensions/components/template/index.md +2 -2
  49. package/content/extensions/components/template/setup-page.md +1 -1
  50. package/content/extensions/components/websocket/api.md +3 -3
  51. package/content/extensions/components/websocket/errors.md +5 -5
  52. package/content/extensions/components/websocket/index.md +5 -5
  53. package/content/extensions/components/websocket/usage.md +3 -3
  54. package/content/extensions/helpers/cron/index.md +2 -2
  55. package/content/extensions/helpers/crypto/index.md +1 -1
  56. package/content/extensions/helpers/env/index.md +27 -12
  57. package/content/extensions/helpers/error/index.md +81 -25
  58. package/content/extensions/helpers/index.md +2 -3
  59. package/content/extensions/helpers/inversion/index.md +15 -7
  60. package/content/extensions/helpers/kafka/compile-binary.md +92 -0
  61. package/content/extensions/helpers/kafka/examples.md +1 -1
  62. package/content/extensions/helpers/kafka/index.md +3 -0
  63. package/content/extensions/helpers/logger/index.md +32 -2
  64. package/content/extensions/helpers/network/index.md +6 -0
  65. package/content/extensions/helpers/queue/index.md +14 -17
  66. package/content/extensions/helpers/redis/index.md +548 -323
  67. package/content/extensions/helpers/socket-io/index.md +14 -10
  68. package/content/extensions/helpers/storage/api.md +44 -8
  69. package/content/extensions/helpers/storage/index.md +43 -7
  70. package/content/extensions/helpers/template/index.md +6 -3
  71. package/content/extensions/helpers/types/index.md +11 -8
  72. package/content/extensions/helpers/websocket/api.md +9 -9
  73. package/content/extensions/helpers/websocket/index.md +7 -7
  74. package/content/extensions/helpers/worker-thread/index.md +2 -2
  75. package/content/extensions/index.md +3 -4
  76. package/content/extensions/src-details/mcp-server.md +18 -24
  77. package/content/guides/core-concepts/application/bootstrapping.md +11 -14
  78. package/content/guides/core-concepts/application/index.md +3 -3
  79. package/content/guides/core-concepts/components.md +19 -10
  80. package/content/guides/core-concepts/dependency-injection.md +6 -3
  81. package/content/guides/core-concepts/grpc-controllers.md +6 -5
  82. package/content/guides/core-concepts/persistent/datasources.md +42 -43
  83. package/content/guides/core-concepts/persistent/index.md +16 -7
  84. package/content/guides/core-concepts/persistent/models.md +24 -20
  85. package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
  86. package/content/guides/core-concepts/persistent/repositories.md +40 -23
  87. package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
  88. package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
  89. package/content/guides/core-concepts/persistent/transactions.md +61 -25
  90. package/content/guides/core-concepts/rest-controllers.md +12 -9
  91. package/content/guides/core-concepts/services.md +330 -60
  92. package/content/guides/get-started/5-minute-quickstart.md +15 -15
  93. package/content/guides/get-started/philosophy.md +36 -36
  94. package/content/guides/get-started/setup.md +3 -3
  95. package/content/guides/index.md +3 -3
  96. package/content/guides/migrations/redis-helpers-migration.md +177 -0
  97. package/content/guides/migrations/scoped-rbac-migration.md +17 -17
  98. package/content/guides/migrations/unified-connectors-migration.md +113 -0
  99. package/content/guides/reference/glossary.md +19 -12
  100. package/content/guides/reference/mcp-docs-server.md +22 -18
  101. package/content/guides/tutorials/building-a-crud-api.md +37 -44
  102. package/content/guides/tutorials/complete-installation.md +17 -17
  103. package/content/guides/tutorials/ecommerce-api.md +163 -124
  104. package/content/guides/tutorials/realtime-chat.md +181 -135
  105. package/content/guides/tutorials/testing.md +65 -523
  106. package/content/index.md +2 -180
  107. package/content/public/apple-touch-icon.png +0 -0
  108. package/content/public/og-image.png +0 -0
  109. package/content/public/site.webmanifest +11 -0
  110. package/content/references/base/application.md +4 -5
  111. package/content/references/base/bootstrapping.md +18 -5
  112. package/content/references/base/components.md +149 -120
  113. package/content/references/base/connectors.md +178 -0
  114. package/content/references/base/controllers.md +41 -30
  115. package/content/references/base/datasources.md +163 -92
  116. package/content/references/base/dependency-injection.md +34 -22
  117. package/content/references/base/filter-system/application-usage.md +17 -14
  118. package/content/references/base/filter-system/array-operators.md +7 -2
  119. package/content/references/base/filter-system/comparison-operators.md +3 -0
  120. package/content/references/base/filter-system/default-filter.md +89 -71
  121. package/content/references/base/filter-system/fields-order-pagination.md +22 -22
  122. package/content/references/base/filter-system/index.md +6 -3
  123. package/content/references/base/filter-system/json-filtering.md +20 -1
  124. package/content/references/base/filter-system/list-operators.md +1 -1
  125. package/content/references/base/filter-system/logical-operators.md +33 -1
  126. package/content/references/base/filter-system/null-operators.md +30 -1
  127. package/content/references/base/filter-system/quick-reference.md +23 -4
  128. package/content/references/base/filter-system/tips.md +5 -5
  129. package/content/references/base/filter-system/use-cases.md +12 -12
  130. package/content/references/base/grpc-controllers.md +13 -13
  131. package/content/references/base/index.md +24 -12
  132. package/content/references/base/middlewares.md +265 -327
  133. package/content/references/base/models.md +63 -49
  134. package/content/references/base/providers.md +136 -130
  135. package/content/references/base/repositories/advanced.md +59 -58
  136. package/content/references/base/repositories/index.md +115 -91
  137. package/content/references/base/repositories/mixins.md +55 -291
  138. package/content/references/base/repositories/relations.md +54 -64
  139. package/content/references/base/repositories/soft-deletable.md +31 -30
  140. package/content/references/base/services.md +296 -93
  141. package/content/references/configuration/environment-variables.md +49 -31
  142. package/content/references/configuration/index.md +6 -6
  143. package/content/references/index.md +17 -12
  144. package/content/references/quick-reference.md +65 -106
  145. package/content/references/utilities/crypto.md +65 -23
  146. package/content/references/utilities/index.md +3 -3
  147. package/content/references/utilities/jsx.md +6 -4
  148. package/content/references/utilities/module.md +68 -20
  149. package/content/references/utilities/parse.md +4 -14
  150. package/content/references/utilities/promise.md +9 -7
  151. package/content/references/utilities/schema.md +5 -3
  152. package/dist/mcp-server/common/guards.d.ts +8 -0
  153. package/dist/mcp-server/common/guards.d.ts.map +1 -0
  154. package/dist/mcp-server/common/guards.js +14 -0
  155. package/dist/mcp-server/common/guards.js.map +1 -0
  156. package/dist/mcp-server/common/index.d.ts +1 -0
  157. package/dist/mcp-server/common/index.d.ts.map +1 -1
  158. package/dist/mcp-server/common/index.js +1 -0
  159. package/dist/mcp-server/common/index.js.map +1 -1
  160. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  161. package/dist/mcp-server/helpers/docs.helper.js +4 -2
  162. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  163. package/dist/mcp-server/helpers/github.helper.js +1 -1
  164. package/dist/mcp-server/index.js +7 -2
  165. package/dist/mcp-server/index.js.map +1 -1
  166. package/dist/mcp-server/tools/base.tool.d.ts +6 -2
  167. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  168. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  169. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  171. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  172. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  173. package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
  174. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  175. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  176. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
  177. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  178. package/package.json +9 -9
  179. package/content/extensions/helpers/testing/index.md +0 -510
  180. package/content/references/base/middleware.md +0 -347
@@ -338,7 +338,7 @@ It supports multiple formats:
338
338
  // String array
339
339
  roles: ['admin', 'user']
340
340
 
341
- // Object array with identifier (preferred matches AuthorizationRole.identifier)
341
+ // Object array with identifier (preferred - matches AuthorizationRole.identifier)
342
342
  roles: [{ id: 1, identifier: '900_admin', priority: 900 }]
343
343
 
344
344
  // Object array with name fallback
@@ -586,24 +586,24 @@ AuthorizationEnforcerRegistry.getInstance().register({
586
586
 
587
587
  ## Custom Filtered Adapter
588
588
 
589
- For most apps, use the ready-made [`ScopedCasbinAdapter`](./api#scopedcasbinadapter) it reads a single
589
+ For most apps, use the ready-made [`ScopedCasbinAdapter`](./api#scopedcasbinadapter) - it reads a single
590
590
  edge table and needs no subclassing. Write a custom adapter only when your storage model differs.
591
591
 
592
592
  `BaseFilteredAdapter` is now thin: it provides the datasource/connector plumbing, the `isFiltered()`
593
593
  flag, no-op write methods, and a `loadLines` helper. A subclass implements **only** `loadFilteredPolicy`
594
- query your store for ONE principal's policies and turn them into casbin lines.
594
+ - query your store for ONE principal's policies and turn them into casbin lines.
595
595
 
596
596
  ```typescript
597
597
  import {
598
598
  BaseFilteredAdapter,
599
599
  ICasbinPolicyFilter,
600
- type IDataSource,
600
+ type ICasbinPolicySource,
601
601
  } from '@venizia/ignis';
602
602
  import type { Model } from 'casbin';
603
603
 
604
604
  // Narrow the filter if you like, or use the default ICasbinPolicyFilter ({ principal: { type, id } }).
605
605
  class MyCustomAdapter extends BaseFilteredAdapter<ICasbinPolicyFilter> {
606
- constructor(opts: { dataSource: IDataSource }) {
606
+ constructor(opts: { dataSource: ICasbinPolicySource }) {
607
607
  super({ scope: MyCustomAdapter.name, dataSource: opts.dataSource });
608
608
  }
609
609
 
@@ -628,7 +628,7 @@ class MyCustomAdapter extends BaseFilteredAdapter<ICasbinPolicyFilter> {
628
628
  ```
629
629
 
630
630
  The base no longer ships template-method hooks (`buildDirectPolicies`/`buildGroupPolicies`/…) or line
631
- formatters you own line construction. See `ScopedCasbinAdapter` for a full reference implementation
631
+ formatters - you own line construction. See `ScopedCasbinAdapter` for a full reference implementation
632
632
  (role closure, structural trees, soft-delete, schema-qualified SQL).
633
633
 
634
634
  ## AuthorizationRole Comparison
@@ -704,7 +704,7 @@ import { MetadataRegistry } from '@venizia/ignis';
704
704
 
705
705
  const registry = MetadataRegistry.getInstance();
706
706
 
707
- // Flat array of principal names ideal for Casbin policy setup
707
+ // Flat array of principal names - ideal for Casbin policy setup
708
708
  const principals = registry.getAuthorizeModelPrincipals({ format: 'array' });
709
709
  // ['article', 'user', 'configuration']
710
710
 
@@ -729,11 +729,11 @@ For multi-tenant apps where a user holds roles **scoped to specific tenants** (a
729
729
 
730
730
  There are two ways to do domain scoping:
731
731
 
732
- - **Scoped model (recommended)** set `isScoped: true` + use `ScopedCasbinAdapter` + the built-in
732
+ - **Scoped model (recommended)** - set `isScoped: true` + use `ScopedCasbinAdapter` + the built-in
733
733
  `CASBIN_RBAC_DOMAIN_SCOPED_MODEL`, and supply the request domain **per route** via `spec.domain` (or a
734
734
  global `domainResolver`). The enforcer registers the matchers for you; you do not write `domainMatching`
735
735
  or `normalizePayloadFn`.
736
- - **Manual flat model (lower-level)** keep a flat `g + p` model and register `domainMatching` +
736
+ - **Manual flat model (lower-level)** - keep a flat `g + p` model and register `domainMatching` +
737
737
  `normalizePayloadFn` yourself. Documented below under [The model](#the-model).
738
738
 
739
739
  ### Scoped model + per-route domain (recommended)
@@ -745,7 +745,7 @@ then tell each route where to read its domain from. `IAuthorizationSpec.domain`
745
745
  ```typescript
746
746
  import type { IAuthorizationDomainSource, TAuthorizationDomainResolver } from '@venizia/ignis';
747
747
 
748
- // (a) Declarative read the domain id from a request param/header/query/context var:
748
+ // (a) Declarative - read the domain id from a request param/header/query/context var:
749
749
  authorize({
750
750
  spec: {
751
751
  action: 'read',
@@ -754,7 +754,7 @@ authorize({
754
754
  },
755
755
  });
756
756
 
757
- // (b) Resolver compute { type, id } yourself (return null → SYSTEM_WIDE):
757
+ // (b) Resolver - compute { type, id } yourself (return null → SYSTEM_WIDE):
758
758
  authorize({
759
759
  spec: {
760
760
  action: 'read',
@@ -803,9 +803,9 @@ e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
803
803
  m = g(r.sub, p.sub, r.dom) && keyMatch(r.dom, p.dom) && r.obj == p.obj && r.act == p.act
804
804
  ```
805
805
 
806
- - `g = _, _, _` the membership relation is **domain-aware** (subject, role, domain).
807
- - `g(r.sub, p.sub, r.dom)` the registered domain matching function decides whether the request domain matches the stored membership domain (this is what makes `*` a wildcard).
808
- - `keyMatch(r.dom, p.dom)` `keyMatch` is a **built-in matcher function** (no registration needed); it lets a permission with `p.dom = "*"` match any request domain.
806
+ - `g = _, _, _` - the membership relation is **domain-aware** (subject, role, domain).
807
+ - `g(r.sub, p.sub, r.dom)` - the registered domain matching function decides whether the request domain matches the stored membership domain (this is what makes `*` a wildcard).
808
+ - `keyMatch(r.dom, p.dom)` - `keyMatch` is a **built-in matcher function** (no registration needed); it lets a permission with `p.dom = "*"` match any request domain.
809
809
 
810
810
  ### Registering the domain matching function
811
811
 
@@ -829,7 +829,7 @@ AuthorizationEnforcerRegistry.getInstance().register({
829
829
  name: 'casbin',
830
830
  type: AuthorizationEnforcerTypes.CASBIN,
831
831
  options: {
832
- model: { driver: CasbinEnforcerModelDrivers.TEXT, definition: CASBIN_RBAC_MODEL },
832
+ model: { driver: CasbinEnforcerModelDrivers.TEXT, definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL },
833
833
  adapter,
834
834
  cached,
835
835
  // For a domain model, normalizePayloadFn MUST always return a `domain`.
@@ -848,7 +848,7 @@ AuthorizationEnforcerRegistry.getInstance().register({
848
848
  ```
849
849
 
850
850
  > [!NOTE]
851
- > `domainMatching` is opt-in. When omitted, domains are compared as exact strings and behavior is unchanged. The enforcer calls Casbin's `addNamedDomainMatchingFunc(roleDefinition, Util.keyMatchFunc)` internally you never call it directly.
851
+ > `domainMatching` is opt-in. When omitted, domains are compared as exact strings and behavior is unchanged. The enforcer calls Casbin's `addNamedDomainMatchingFunc(roleDefinition, Util.keyMatchFunc)` internally - you never call it directly.
852
852
 
853
853
  ### Choosing the matching function
854
854
 
@@ -861,26 +861,26 @@ domainMatching: { roleDefinition: 'g', fn: CasbinDomainMatchingFunctions.KEY_MAT
861
861
  domainMatching: { roleDefinition: 'g', fn: CasbinDomainMatchingFunctions.REGEX_MATCH }; // ^Merchant_.*$
862
862
  ```
863
863
 
864
- ### All cases policy lines and outcomes
864
+ ### All cases - policy lines and outcomes
865
865
 
866
866
  Given the model above with `keyMatch` registered on `g`, the request is `enforceSync(subject, domain, resource, action)`:
867
867
 
868
868
  | Case | Policy lines | Request | Outcome |
869
869
  |------|--------------|---------|---------|
870
870
  | **Scoped role** (owner/employee in a tenant) | `g, User_u, Role_owner, Merchant_A`<br/>`p, Role_owner, *, Material.find, read, allow` | `(User_u, Merchant_A, Material.find, read)` | ✅ allow |
871
- | **Scoped role isolation** | (same as above) | `(User_u, Merchant_B, Material.find, read)` | ❌ deny (`g` domain doesn't match) |
871
+ | **Scoped role - isolation** | (same as above) | `(User_u, Merchant_B, Material.find, read)` | ❌ deny (`g` domain doesn't match) |
872
872
  | **Multi-tenant role** | `g, User_u, Role_owner, Merchant_A`<br/>`g, User_u, Role_owner, Merchant_B`<br/>`p, Role_owner, *, Material.find, read, allow` | `(User_u, Merchant_B, Material.find, read)` | ✅ allow (one `g` line per owned tenant; **single** `p` line) |
873
873
  | **Global role** (e.g. guest/onboarding) | `g, User_u, Role_guest, *`<br/>`p, Role_guest, *, Organizer.onBoarding, create, allow` | `(User_u, Merchant_anything, Organizer.onBoarding, create)` | ✅ allow (wildcard `g` domain) |
874
874
  | **Direct user permission (scoped)** | `p, User_u, Merchant_A, Report.read, read, allow` | `(User_u, Merchant_A, Report.read, read)` | ✅ allow (reflexive `g(u,u,dom)` + `keyMatch`) |
875
- | **Direct user permission isolation** | (same as above) | `(User_u, Merchant_B, Report.read, read)` | ❌ deny |
875
+ | **Direct user permission - isolation** | (same as above) | `(User_u, Merchant_B, Report.read, read)` | ❌ deny |
876
876
  | **Deny override** | `p, Role_x, *, Secret.read, read, deny`<br/>`p, Role_y, *, Secret.read, read, allow` | any domain where the user has both roles | ❌ deny (`!some(p.eft == deny)`) |
877
877
 
878
878
  > [!IMPORTANT]
879
- > The function is applied as `fn(requestDomain, policyDomain)` the wildcard belongs on the **stored** side. Store only `*` or exact domain values (never `Merchant_*`) to keep isolation guaranteed.
879
+ > The function is applied as `fn(requestDomain, policyDomain)` - the wildcard belongs on the **stored** side. Store only `*` or exact domain values (never `Merchant_*`) to keep isolation guaranteed.
880
880
 
881
881
  ### Misconfiguration is caught early
882
882
 
883
- If `roleDefinition` is not declared under `[role_definition]` in the model, `configure()` throws Casbin would otherwise register the function as a silent no-op, leaving wildcard domains permanently unmatched (global roles silently denied):
883
+ If `roleDefinition` is not declared under `[role_definition]` in the model, `configure()` throws - Casbin would otherwise register the function as a silent no-op, leaving wildcard domains permanently unmatched (global roles silently denied):
884
884
 
885
885
  ```typescript
886
886
  // model declares `g` only
@@ -200,7 +200,7 @@ Response `200`:
200
200
 
201
201
  ### Route Definition Patterns
202
202
 
203
- The `HealthCheckController` demonstrates all three route definition patterns supported by Ignis:
203
+ The `HealthCheckController` demonstrates all three route definition patterns supported by IGNIS:
204
204
 
205
205
  | Pattern | Method | Used For |
206
206
  |---------|--------|----------|
@@ -1,6 +1,6 @@
1
1
  # Components
2
2
 
3
- Reusable, pluggable modules that group together related features. A component can encapsulate various resources such as providers, services, controllers, repositories, or even an entire mini-application, providing a clean way to modularize and share complex logic across Ignis applications.
3
+ Reusable, pluggable modules that group together related features. A component can encapsulate various resources such as providers, services, controllers, repositories, or even an entire mini-application, providing a clean way to modularize and share complex logic across IGNIS applications.
4
4
 
5
5
  ## Built-in Components
6
6
 
@@ -13,8 +13,8 @@ Reusable, pluggable modules that group together related features. A component ca
13
13
  | [Request Tracker](./request-tracker) | Request logging | Request ID generation, timing, structured logging |
14
14
  | [Socket.IO](./socket-io/) | Real-time communication | WebSocket support, Redis adapter, event-based |
15
15
  | [WebSocket](./websocket/) | Real-time communication | Bun native WebSocket, Redis Pub/Sub, heartbeat |
16
- | [Static Asset](./static-asset/) | File management | Upload/download files, MinIO & local filesystem support |
17
- | [Swagger](./swagger) | API documentation | OpenAPI generation, Swagger UI, Scalar UI |
16
+ | [Static Asset](./static-asset/) | File management | Upload/download files, MinIO, Bun S3 & local filesystem support |
17
+ | [Swagger](./api-reference) | API documentation | OpenAPI generation, Swagger UI, Scalar UI |
18
18
  | [gRPC](/references/base/grpc-controllers) | gRPC transport | ConnectRPC integration, unary RPC, decorator-based |
19
19
 
20
20
  ## Creating a Component
@@ -80,8 +80,8 @@ Using components is a great way to organize your application's features into mod
80
80
  - [Request Tracker](./request-tracker) - Request tracking
81
81
  - [Socket.IO](./socket-io/) - Socket.IO WebSocket support
82
82
  - [WebSocket](./websocket/) - Bun native WebSocket
83
- - [Static Asset](./static-asset/) - Static file serving
84
- - [Swagger](./swagger) - API documentation
83
+ - [Static Asset](./static-asset/) - File upload/download management
84
+ - [Swagger](./api-reference) - API documentation
85
85
  - [gRPC](/references/base/grpc-controllers) - gRPC transport (ConnectRPC)
86
86
 
87
87
  - **References:**
@@ -10,13 +10,13 @@ All errors are created via `getError()` and include `statusCode`, `messageCode`,
10
10
 
11
11
  | Condition | Status | Error Code | Message |
12
12
  |-----------|--------|-----------|---------|
13
- | `to` is missing or empty array | 400 | `MAIL_INVALID_RECIPIENT` | `Recipient email address is required` |
14
- | `subject` is falsy | 400 | `MAIL_INVALID_CONFIGURATION` | `Email subject is required` |
15
- | Both `text` and `html` are falsy | 400 | `MAIL_INVALID_CONFIGURATION` | `Email must have either text or html content` |
16
- | Transport throws during `send()` | 500 | `MAIL_SEND_FAILED` | `Failed to send email: <error>` |
17
- | Batch operation fails | 500 | `MAIL_BATCH_SEND_FAILED` | `Failed to send batch emails: <error>` |
18
- | Template engine not configured for `sendTemplate()` | 500 | `MAIL_INVALID_CONFIGURATION` | `Template engine not configured` |
19
- | Transport throws during `verify()` | 500 | `MAIL_VERIFICATION_FAILED` | `Mail transport verification failed: <error>` |
13
+ | `to` is missing or empty array | 400 | `core.mail.invalid_recipient` | `Recipient email address is required` |
14
+ | `subject` is falsy | 400 | `core.mail.invalid_configuration` | `Email subject is required` |
15
+ | Both `text` and `html` are falsy | 400 | `core.mail.invalid_configuration` | `Email must have either text or html content` |
16
+ | Transport throws during `send()` | 500 | `core.mail.send_failed` | `Failed to send email: <error>` |
17
+ | Batch operation fails | 500 | `core.mail.batch_send_failed` | `Failed to send batch emails: <error>` |
18
+ | Template engine not configured for `sendTemplate()` | 500 | `core.mail.invalid_configuration` | `Template engine not configured` |
19
+ | Transport throws during `verify()` | 500 | `core.mail.verification_failed` | `Mail transport verification failed: <error>` |
20
20
 
21
21
  ### MailComponent Errors
22
22
 
@@ -28,11 +28,11 @@ All errors are created via `getError()` and include `statusCode`, `messageCode`,
28
28
 
29
29
  | Condition | Status | Error Code | Message |
30
30
  |-----------|--------|-----------|---------|
31
- | Unsupported provider string | 500 | `MAIL_INVALID_CONFIGURATION` | `Unsupported mail provider: <provider>` |
32
- | Nodemailer options fail type guard | 500 | `MAIL_INVALID_CONFIGURATION` | `Invalid Nodemailer configuration` |
33
- | Mailgun options fail type guard | 500 | `MAIL_INVALID_CONFIGURATION` | `Invalid Mailgun configuration` |
34
- | Custom options fail type guard | 500 | `MAIL_INVALID_CONFIGURATION` | `Invalid custom mail provider configuration` |
35
- | Custom config missing `send`/`verify` | 500 | `MAIL_INVALID_CONFIGURATION` | `Custom mail provider must implement IMailTransport interface. Missing methods: <methods>` |
31
+ | Unsupported provider string | 500 | `core.mail.invalid_configuration` | `Unsupported mail provider: <provider>` |
32
+ | Nodemailer options fail type guard | 500 | `core.mail.invalid_configuration` | `Invalid Nodemailer configuration` |
33
+ | Mailgun options fail type guard | 500 | `core.mail.invalid_configuration` | `Invalid Mailgun configuration` |
34
+ | Custom options fail type guard | 500 | `core.mail.invalid_configuration` | `Invalid custom mail provider configuration` |
35
+ | Custom config missing `send`/`verify` | 500 | `core.mail.invalid_configuration` | `Custom mail provider must implement IMailTransport interface. Missing methods: <methods>` |
36
36
 
37
37
  ### MailQueueExecutorProvider Errors
38
38
 
@@ -47,8 +47,8 @@ All errors are created via `getError()` and include `statusCode`, `messageCode`,
47
47
  | Condition | Status | Error Code | Message |
48
48
  |-----------|--------|-----------|---------|
49
49
  | Neither `templateName` nor `templateData` provided | -- | -- | `Either templateName or templateData must be provided` |
50
- | Template name not found in registry | 404 | `TEMPLATE_NOT_FOUND` | `Template not found: <name>` |
51
- | Missing template data keys (with `requireValidate: true`) | 400 | `MAIL_INVALID_CONFIGURATION` | `Missing template data for keys: <keys>` |
50
+ | Template name not found in registry | 404 | `core.mail.template_not_found` | `Template not found: <name>` |
51
+ | Missing template data keys (with `requireValidate: true`) | 400 | `core.mail.invalid_configuration` | `Missing template data for keys: <keys>` |
52
52
 
53
53
  ### Queue Executor Errors
54
54
 
@@ -78,7 +78,7 @@ this.application.bind({ key: MailKeys.MAIL_OPTIONS }).toValue({
78
78
  this.application.component(MailComponent);
79
79
  ```
80
80
 
81
- ### "TEMPLATE_NOT_FOUND" when calling `sendTemplate()`
81
+ ### "core.mail.template_not_found" when calling `sendTemplate()`
82
82
 
83
83
  **Cause:** The template name passed to `sendTemplate()` was never registered via `templateEngine.registerTemplate()`.
84
84
 
@@ -107,9 +107,8 @@ import {
107
107
  Binding,
108
108
  CoreBindings,
109
109
  inject,
110
- applicationEnvironment,
111
- toBoolean,
112
110
  } from '@venizia/ignis';
111
+ import { applicationEnvironment, toBoolean } from '@venizia/ignis-helpers';
113
112
  import { MailComponent, MailKeys, MailProviders } from '@venizia/ignis/mail';
114
113
 
115
114
  export class NodemailerComponent extends BaseComponent {
@@ -81,7 +81,7 @@ await mailService.send({
81
81
  subject: '', // Empty subject triggers validation error
82
82
  html: '<p>Hello</p>',
83
83
  });
84
- // Error: { statusCode: 400, messageCode: 'MAIL_INVALID_CONFIGURATION', message: 'Email subject is required' }
84
+ // Error: { statusCode: 400, messageCode: 'core.mail.invalid_configuration', message: 'Email subject is required' }
85
85
  ```
86
86
 
87
87
  ## Template Engine
@@ -67,7 +67,7 @@ The log format follows this structure:
67
67
  The HTTP method is padded to 8 characters for consistent alignment in log output.
68
68
 
69
69
  > [!TIP]
70
- > The request ID is also available in error middleware contexts (`NotFoundMiddleware`, `AppErrorMiddleware`), making it easy to correlate error logs with the original request.
70
+ > The request ID is also available in the framework's error handlers (`notFoundHandler`, `appErrorHandler`), making it easy to correlate error logs with the original request.
71
71
 
72
72
  ## Configuration
73
73
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Architecture
6
6
 
7
- The component integrates Socket.IO into the Ignis application lifecycle with runtime-specific initialization (Node.js vs Bun).
7
+ The component integrates Socket.IO into the IGNIS application lifecycle with runtime-specific initialization (Node.js vs Bun).
8
8
 
9
9
  #### Architecture Diagram
10
10
  ```
@@ -170,7 +170,7 @@ The helper uses a **discriminated union** for its constructor options, keyed on
170
170
  interface ISocketIOServerBaseOptions {
171
171
  identifier: string;
172
172
  serverOptions: Partial<ServerOptions>;
173
- redisConnection: DefaultRedisHelper;
173
+ redisConnection: IRedisHelper;
174
174
  defaultRooms?: string[]; // Default: ['io-default', 'io-notification']
175
175
  authenticateTimeout?: number; // Default: 10_000 (10 seconds)
176
176
  pingInterval?: number; // Default: 30_000 (30 seconds)
@@ -201,7 +201,7 @@ During construction:
201
201
  4. Calls `initRedisClients()` -- creates 3 duplicated Redis clients from the connection
202
202
 
203
203
  > [!IMPORTANT]
204
- > Redis clients are **duplicated** from the parent connection (`client.duplicate()`). This means the helper uses 3 independent connections (pub, sub, emitter) that inherit config from the parent but maintain separate state. The parent `RedisHelper` connection is not consumed.
204
+ > Redis clients are **duplicated** from the parent connection (`client.duplicate()`). This means the helper uses 3 independent connections (pub, sub, emitter) that inherit config from the parent but maintain separate state. The parent `RedisSingleHelper` connection is not consumed.
205
205
 
206
206
  ### `configure()` -- Server Initialization
207
207
 
@@ -629,7 +629,7 @@ Reads all binding keys from the DI container and validates required ones:
629
629
  | Binding | Validation | Error on Failure |
630
630
  |---------|-----------|------------------|
631
631
  | `SERVER_OPTIONS` | Optional, merged with defaults via `Object.assign()` | -- |
632
- | `REDIS_CONNECTION` | Must be `instanceof DefaultRedisHelper` | `"Invalid instance of redisConnection | Please init connection with RedisHelper for single redis connection or RedisClusterHelper for redis cluster mode!"` |
632
+ | `REDIS_CONNECTION` | Must be `instanceof AbstractRedisHelper` | `"Invalid instance of redisConnection | Please init connection with RedisSingleHelper (single), RedisClusterHelper (cluster), or RedisSentinelHelper (sentinel)"` |
633
633
  | `AUTHENTICATE_HANDLER` | Must be a function (non-null) | `"[DANGER][SocketIOComponent] Invalid authenticateFn to setup io socket server!"` |
634
634
  | `VALIDATE_ROOM_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` | -- |
635
635
  | `CLIENT_CONNECTED_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` | -- |
@@ -681,10 +681,10 @@ Node mode is simpler because Socket.IO natively attaches to `node:http.Server`.
681
681
 
682
682
  ### Redis 3-Client Architecture
683
683
 
684
- The server helper creates 3 independent Redis connections from a single `DefaultRedisHelper`:
684
+ The server helper creates 3 independent Redis connections from a single `AbstractRedisHelper` instance:
685
685
 
686
686
  ```
687
- RedisHelper (parent -- NOT consumed)
687
+ RedisSingleHelper (parent -- NOT consumed)
688
688
  |
689
689
  +-- client.duplicate() --> redisPub (for Redis adapter -- publishes)
690
690
  |
@@ -696,7 +696,7 @@ RedisHelper (parent -- NOT consumed)
696
696
  **Why 3 clients?**
697
697
  - `@socket.io/redis-adapter` requires separate pub and sub clients because a Redis connection in subscribe mode cannot execute other commands
698
698
  - `@socket.io/redis-emitter` uses its own client to emit messages independently of the adapter, enabling cross-instance broadcasting even from contexts without a direct Socket.IO reference
699
- - The parent `RedisHelper` connection remains independent and is not consumed -- it can be used for other purposes (e.g., caching, sessions)
699
+ - The parent `RedisSingleHelper` connection remains independent and is not consumed -- it can be used for other purposes (e.g., caching, sessions)
700
700
 
701
701
  **`TRedisClient` type:**
702
702
  ```typescript
@@ -826,7 +826,7 @@ type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOpt
826
826
  interface ISocketIOServerBaseOptions {
827
827
  identifier: string;
828
828
  serverOptions: Partial<ServerOptions>;
829
- redisConnection: DefaultRedisHelper;
829
+ redisConnection: IRedisHelper;
830
830
  defaultRooms?: string[];
831
831
  authenticateTimeout?: number;
832
832
  pingInterval?: number;
@@ -915,7 +915,7 @@ interface IServerOptions extends ServerOptions {
915
915
 
916
916
  // Resolved binding values from DI container
917
917
  interface IResolvedBindings {
918
- redisConnection: DefaultRedisHelper;
918
+ redisConnection: IRedisHelper;
919
919
  authenticateFn: TSocketIOAuthenticateFn;
920
920
  validateRoomFn?: TSocketIOValidateRoomFn;
921
921
  clientConnectedFn?: TSocketIOClientConnectedFn;
@@ -10,7 +10,7 @@
10
10
  |--------|-----------|---------------|
11
11
  | `binding()` | `application` is falsy | `"[binding] Invalid application to bind SocketIOComponent"` |
12
12
  | `binding()` | Unsupported runtime | `"[SocketIOComponent] Unsupported runtime: <runtime>"` |
13
- | `resolveBindings()` | `REDIS_CONNECTION` not instanceof `DefaultRedisHelper` | `"Invalid instance of redisConnection | Please init connection with RedisHelper for single redis connection or RedisClusterHelper for redis cluster mode!"` |
13
+ | `resolveBindings()` | `REDIS_CONNECTION` not instanceof `AbstractRedisHelper` | `"Invalid instance of redisConnection | Please init connection with RedisSingleHelper for single redis connection, RedisClusterHelper for cluster mode, or RedisSentinelHelper for sentinel mode!"` |
14
14
  | `resolveBindings()` | `AUTHENTICATE_HANDLER` is falsy | `"[DANGER][SocketIOComponent] Invalid authenticateFn to setup io socket server!"` |
15
15
  | `registerNodeHook()` | HTTP server not available | `"[SocketIOComponent] HTTP server not available for Node.js runtime!"` |
16
16
 
@@ -54,16 +54,16 @@
54
54
 
55
55
  ### "Invalid instance of redisConnection"
56
56
 
57
- **Cause**: The value bound to `REDIS_CONNECTION` is not an instance of `DefaultRedisHelper` (or its subclasses `RedisHelper` / `RedisClusterHelper`).
57
+ **Cause**: The value bound to `REDIS_CONNECTION` is not an instance of `AbstractRedisHelper` (i.e. not a `RedisSingleHelper`, `RedisClusterHelper`, or `RedisSentinelHelper`).
58
58
 
59
- **Fix**: Use `RedisHelper` (single instance) or `RedisClusterHelper` (cluster mode):
59
+ **Fix**: Use one of the concrete topology helpers:
60
60
 
61
61
  ```typescript
62
62
  import { SocketIOBindingKeys } from '@venizia/ignis/socket-io';
63
63
 
64
64
  // Correct -- single instance
65
65
  this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION })
66
- .toValue(new RedisHelper({ name: 'socket-io', host, port, password }));
66
+ .toValue(new RedisSingleHelper({ name: 'socket-io', host, port, password }));
67
67
 
68
68
  // Correct -- cluster mode
69
69
  this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION })
@@ -71,7 +71,7 @@ this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION })
71
71
 
72
72
  // Wrong -- raw ioredis client
73
73
  this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION })
74
- .toValue(new Redis(6379)); // This is NOT a DefaultRedisHelper!
74
+ .toValue(new Redis(6379)); // This is NOT an AbstractRedisHelper!
75
75
  ```
76
76
 
77
77
  ### "Cannot find module '@socket.io/bun-engine'"
@@ -76,7 +76,7 @@ import {
76
76
  SocketIOComponent,
77
77
  SocketIOBindingKeys,
78
78
  } from '@venizia/ignis/socket-io';
79
- import { RedisHelper, ValueOrPromise } from '@venizia/ignis-helpers';
79
+ import { RedisSingleHelper, ValueOrPromise } from '@venizia/ignis-helpers';
80
80
  import type {
81
81
  TSocketIOAuthenticateFn,
82
82
  TSocketIOValidateRoomFn,
@@ -84,7 +84,7 @@ import type {
84
84
  } from '@venizia/ignis-helpers/socket-io';
85
85
 
86
86
  export class Application extends BaseApplication {
87
- private redisHelper: RedisHelper;
87
+ private redisHelper: RedisSingleHelper;
88
88
 
89
89
  preConfigure(): ValueOrPromise<void> {
90
90
  this.setupSocketIO();
@@ -93,7 +93,7 @@ export class Application extends BaseApplication {
93
93
 
94
94
  setupSocketIO() {
95
95
  // 1. Redis connection (required for adapter + emitter)
96
- this.redisHelper = new RedisHelper({
96
+ this.redisHelper = new RedisSingleHelper({
97
97
  name: 'socket-io-redis',
98
98
  host: process.env.REDIS_HOST ?? 'localhost',
99
99
  port: +(process.env.REDIS_PORT ?? 6379),
@@ -101,7 +101,7 @@ export class Application extends BaseApplication {
101
101
  autoConnect: false,
102
102
  });
103
103
 
104
- this.bind<RedisHelper>({
104
+ this.bind<RedisSingleHelper>({
105
105
  key: SocketIOBindingKeys.REDIS_CONNECTION,
106
106
  }).toValue(this.redisHelper);
107
107
 
@@ -145,11 +145,11 @@ export class Application extends BaseApplication {
145
145
 
146
146
  #### `autoConnect: false` Rationale
147
147
 
148
- The `RedisHelper` is created with `autoConnect: false` because the server helper internally calls `client.duplicate()` to create 3 independent Redis connections (pub, sub, emitter). The duplicated clients inherit the `lazyConnect` setting from the parent. During `configure()`, the helper detects clients in `wait` status and explicitly calls `client.connect()` on each, then awaits all 3 to reach `ready` status before proceeding. This avoids race conditions where the parent connects before the duplicates are created.
148
+ The `RedisSingleHelper` is created with `autoConnect: false` because the server helper internally calls `client.duplicate()` to create 3 independent Redis connections (pub, sub, emitter). The duplicated clients inherit the `lazyConnect` setting from the parent. During `configure()`, the helper detects clients in `wait` status and explicitly calls `client.connect()` on each, then awaits all 3 to reach `ready` status before proceeding. This avoids race conditions where the parent connects before the duplicates are created.
149
149
 
150
150
  #### Redis Connection Alternatives
151
151
 
152
- You can use either `RedisHelper` (single Redis instance) or `RedisClusterHelper` (Redis Cluster mode). Both extend `DefaultRedisHelper`, which is the type the component validates against:
152
+ You can use `RedisSingleHelper` (single Redis instance), `RedisClusterHelper` (Redis Cluster mode), or `RedisSentinelHelper` (Sentinel HA). All extend `AbstractRedisHelper` and satisfy the `IRedisHelper` interface that the component validates against:
153
153
 
154
154
  ```typescript
155
155
  import { RedisClusterHelper } from '@venizia/ignis-helpers';
@@ -258,7 +258,7 @@ All binding keys are available in `SocketIOBindingKeys`:
258
258
  | Binding Key | Constant | Type | Required | Default |
259
259
  |------------|----------|------|----------|---------|
260
260
  | `@app/socket-io/server-options` | `SERVER_OPTIONS` | `Partial<ServerOptions>` | No | See defaults above |
261
- | `@app/socket-io/redis-connection` | `REDIS_CONNECTION` | `RedisHelper` / `RedisClusterHelper` / `DefaultRedisHelper` | **Yes** | `null` |
261
+ | `@app/socket-io/redis-connection` | `REDIS_CONNECTION` | `IRedisHelper` (`RedisSingleHelper` / `RedisClusterHelper` / `RedisSentinelHelper`) | **Yes** | `null` |
262
262
  | `@app/socket-io/authenticate-handler` | `AUTHENTICATE_HANDLER` | `TSocketIOAuthenticateFn` | **Yes** | `null` |
263
263
  | `@app/socket-io/validate-room-handler` | `VALIDATE_ROOM_HANDLER` | `TSocketIOValidateRoomFn` | No | `null` |
264
264
  | `@app/socket-io/client-connected-handler` | `CLIENT_CONNECTED_HANDLER` | `TSocketIOClientConnectedFn` | No | `null` |
@@ -360,7 +360,7 @@ The component resolves all binding keys into a single `IResolvedBindings` object
360
360
  #### `IResolvedBindings` Interface
361
361
  ```typescript
362
362
  interface IResolvedBindings {
363
- redisConnection: DefaultRedisHelper;
363
+ redisConnection: IRedisHelper;
364
364
  authenticateFn: TSocketIOAuthenticateFn;
365
365
  validateRoomFn?: TSocketIOValidateRoomFn;
366
366
  clientConnectedFn?: TSocketIOClientConnectedFn;
@@ -128,7 +128,7 @@ The constructor immediately calls `configure()`, which creates the `socket.io-cl
128
128
 
129
129
  #### `connect` vs `connection` Event
130
130
 
131
- The client-side `socket.io-client` library fires the `connect` event (no "ion" suffix) when the connection is established. The server-side `socket.io` library fires `connection` (with the suffix). This is a Socket.IO convention, not an Ignis-specific behavior. The client helper registers on `'connect'` while the server helper registers on `SocketIOConstants.EVENT_CONNECT` which equals `'connection'`.
131
+ The client-side `socket.io-client` library fires the `connect` event (no "ion" suffix) when the connection is established. The server-side `socket.io` library fires `connection` (with the suffix). This is a Socket.IO convention, not an IGNIS-specific behavior. The client helper registers on `'connect'` while the server helper registers on `SocketIOConstants.EVENT_CONNECT` which equals `'connection'`.
132
132
 
133
133
  ### Authentication Flow
134
134
 
@@ -40,7 +40,7 @@ interface IAssetControllerOptions {
40
40
 
41
41
  ## StaticAssetStorageTypes
42
42
 
43
- A constants class following the Ignis pattern with `static readonly` fields, a `SCHEME_SET`, and an `isValid()` method:
43
+ A constants class following the IGNIS pattern with `static readonly` fields, a `SCHEME_SET`, and an `isValid()` method:
44
44
 
45
45
  ```typescript
46
46
  class StaticAssetStorageTypes {
@@ -112,6 +112,17 @@ Content-Disposition: attachment; filename="..." (download endpoint only)
112
112
 
113
113
  Whitelisted metadata headers forwarded from storage: `content-type`, `content-encoding`, `cache-control`, `etag`, `last-modified`. All other metadata headers are dropped. Header values are sanitized (see [Header Sanitization](#header-sanitization)).
114
114
 
115
+ ## Object Name Decoding
116
+
117
+ Hono already percent-decodes the `:objectName` path param before the handler reads it, so the controller does not decode it again - the value read from `ctx.req.valid<TObjectParams>('param')` is used as-is.
118
+
119
+ A prior second `decodeURIComponent()` was actively wrong:
120
+
121
+ - `report_100%.pdf` is a legal object name. Its link is `.../objects/report_100%25.pdf`; Hono hands the handler back `report_100%.pdf`, and a second decode would hit the invalid escape `%.p` and throw - the object would become unfetchable and undeletable.
122
+ - An object named `a%2Fb.png` would decode twice into `a/b.png` - a different object than the one requested.
123
+
124
+ `isValidName()`/`isValidPath()` still run on the (singly-decoded) value, so a traversal payload is rejected exactly as before - what changes is that legal names with `%` in them stop being mangled.
125
+
115
126
  ## IStorageHelper Interface
116
127
 
117
128
  All storage helpers implement this unified interface:
@@ -119,6 +130,7 @@ All storage helpers implement this unified interface:
119
130
  ```typescript
120
131
  interface IStorageHelper {
121
132
  isValidName(name: string): boolean;
133
+ isValidPath(pathStr: string, opts?: { maxDepth?: number }): boolean;
122
134
 
123
135
  // Bucket operations
124
136
  isBucketExists(opts: { name: string }): Promise<boolean>;
@@ -131,7 +143,7 @@ interface IStorageHelper {
131
143
  upload(opts: {
132
144
  bucket: string;
133
145
  files: IUploadFile[];
134
- normalizeNameFn?: (opts: { originalName: string }) => string;
146
+ normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
135
147
  normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
136
148
  }): Promise<IUploadResult[]>;
137
149
 
@@ -155,6 +167,7 @@ interface IUploadFile {
155
167
  buffer: Buffer;
156
168
  size: number;
157
169
  encoding?: string;
170
+ folderPath?: string;
158
171
  [key: string | symbol]: any;
159
172
  }
160
173
 
@@ -214,8 +227,8 @@ BaseStorageHelper (abstract class)
214
227
  | Field | Type | Nullable | Default | Description |
215
228
  |-------|------|----------|---------|-------------|
216
229
  | `id` | TEXT | No | -- | Primary key (UUID) |
217
- | `created_at` | TIMESTAMP | No | `NOW()` | When record was created |
218
- | `modified_at` | TIMESTAMP | No | `NOW()` | When record was last updated |
230
+ | `created_at` | TIMESTAMPTZ | No | `NOW()` | When record was created |
231
+ | `modified_at` | TIMESTAMPTZ | No | `NOW()` | When record was last updated |
219
232
  | `bucket_name` | TEXT | No | -- | Storage bucket name |
220
233
  | `object_name` | TEXT | No | -- | File object name |
221
234
  | `link` | TEXT | No | -- | Access URL to the file |
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Name Validation
6
6
 
7
- All bucket and object names go through `helper.isValidName()` before any storage operation. The validation blocks:
7
+ Bucket names are validated with `helper.isValidName()` (single segment - no path separators allowed). Object names, which may include folder segments (e.g., `2025/uploads/report.pdf`), are validated with `helper.isValidPath()`. The validation blocks:
8
8
 
9
9
  | Pattern | Example | Reason |
10
10
  |---------|---------|--------|
@@ -15,18 +15,18 @@ All bucket and object names go through `helper.isValidName()` before any storage
15
15
  | Long names | 256+ chars | Exceeds 255 character limit |
16
16
  | Empty names | `""`, `" "` | Empty or whitespace-only |
17
17
 
18
- Every endpoint that accepts `bucketName` validates it and returns HTTP 400 `"Invalid bucket name"` on failure. Every endpoint that accepts `objectName` validates it separately and returns HTTP 400 `"Invalid object name"` on failure.
18
+ Every endpoint that accepts `bucketName` validates it and returns HTTP 400 `"Invalid bucket name"` on failure. Every endpoint that accepts `objectName` validates it separately and returns HTTP 400 `"Invalid object name or path"` on failure.
19
19
 
20
20
  ## Troubleshooting
21
21
 
22
22
  ### "Invalid bucket/object name"
23
23
 
24
- **Cause:** The name fails `isValidName()` validation. Names cannot contain `..`, `/`, `\`, shell special characters, or start with `.`. Names must be <= 255 characters and non-empty.
24
+ **Cause:** Bucket name validation: fails `isValidName()` - bucket names are single segments and cannot contain `..`, `/`, or `\`, shell special characters, or start with `.`, and must be <= 255 characters. Object name validation: fails `isValidPath()` - object names may contain `/` for folder structure but each segment must pass the same single-segment rules, and the folder depth must not exceed the configured limit.
25
25
 
26
26
  **Fix:** Ensure names follow these rules:
27
27
  - No path separators (`..`, `/`, `\`)
28
28
  - No leading dot (`.hidden`)
29
- - No shell special characters (`;`, `|`, `&`, `$`, `` ` ``, `<`, `>`, `(`, `)`, `{`, `}`, `[`, `]`, `!`, `#`)
29
+ - No shell special characters (`;`, `|`, `&`, `$`, `` ` ``, `<`, `>`, `{`, `}`, `[`, `]`, `!`, `#`)
30
30
  - No control characters (`\n`, `\r`, `\0`)
31
31
  - 255 characters or fewer
32
32
  - Not empty or whitespace-only