@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.
- package/README.md +7 -7
- package/content/best-practices/api-usage-examples.md +15 -12
- package/content/best-practices/architectural-patterns.md +70 -78
- package/content/best-practices/architecture-decisions.md +91 -60
- package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
- package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
- package/content/best-practices/code-style-standards/control-flow.md +5 -2
- package/content/best-practices/code-style-standards/documentation.md +13 -13
- package/content/best-practices/code-style-standards/function-patterns.md +9 -10
- package/content/best-practices/code-style-standards/index.md +1 -1
- package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
- package/content/best-practices/code-style-standards/route-definitions.md +30 -12
- package/content/best-practices/code-style-standards/tooling.md +8 -5
- package/content/best-practices/code-style-standards/type-safety.md +13 -12
- package/content/best-practices/common-pitfalls.md +56 -37
- package/content/best-practices/contribution-workflow.md +13 -14
- package/content/best-practices/data-modeling.md +46 -22
- package/content/best-practices/deployment-strategies.md +28 -27
- package/content/best-practices/error-handling.md +48 -24
- package/content/best-practices/index.md +5 -5
- package/content/best-practices/performance-optimization.md +40 -31
- package/content/best-practices/security-guidelines.md +52 -23
- package/content/best-practices/testing-strategies.md +65 -51
- package/content/best-practices/troubleshooting-tips.md +24 -24
- package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
- package/content/extensions/components/authentication/api.md +19 -19
- package/content/extensions/components/authentication/errors.md +7 -7
- package/content/extensions/components/authentication/index.md +10 -8
- package/content/extensions/components/authentication/usage.md +101 -6
- package/content/extensions/components/authorization/api.md +45 -25
- package/content/extensions/components/authorization/errors.md +6 -6
- package/content/extensions/components/authorization/index.md +11 -10
- package/content/extensions/components/authorization/usage.md +21 -21
- package/content/extensions/components/health-check.md +1 -1
- package/content/extensions/components/index.md +5 -5
- package/content/extensions/components/mail/errors.md +15 -15
- package/content/extensions/components/mail/index.md +1 -2
- package/content/extensions/components/mail/usage.md +1 -1
- package/content/extensions/components/request-tracker.md +1 -1
- package/content/extensions/components/socket-io/api.md +9 -9
- package/content/extensions/components/socket-io/errors.md +5 -5
- package/content/extensions/components/socket-io/index.md +8 -8
- package/content/extensions/components/socket-io/usage.md +1 -1
- package/content/extensions/components/static-asset/api.md +17 -4
- package/content/extensions/components/static-asset/errors.md +4 -4
- package/content/extensions/components/static-asset/index.md +26 -28
- package/content/extensions/components/static-asset/usage.md +13 -12
- package/content/extensions/components/template/index.md +2 -2
- package/content/extensions/components/template/setup-page.md +1 -1
- package/content/extensions/components/websocket/api.md +3 -3
- package/content/extensions/components/websocket/errors.md +5 -5
- package/content/extensions/components/websocket/index.md +5 -5
- package/content/extensions/components/websocket/usage.md +3 -3
- package/content/extensions/helpers/cron/index.md +2 -2
- package/content/extensions/helpers/crypto/index.md +1 -1
- package/content/extensions/helpers/env/index.md +27 -12
- package/content/extensions/helpers/error/index.md +81 -25
- package/content/extensions/helpers/index.md +2 -3
- package/content/extensions/helpers/inversion/index.md +15 -7
- package/content/extensions/helpers/kafka/compile-binary.md +92 -0
- package/content/extensions/helpers/kafka/examples.md +1 -1
- package/content/extensions/helpers/kafka/index.md +3 -0
- package/content/extensions/helpers/logger/index.md +32 -2
- package/content/extensions/helpers/network/index.md +6 -0
- package/content/extensions/helpers/queue/index.md +14 -17
- package/content/extensions/helpers/redis/index.md +548 -323
- package/content/extensions/helpers/socket-io/index.md +14 -10
- package/content/extensions/helpers/storage/api.md +44 -8
- package/content/extensions/helpers/storage/index.md +43 -7
- package/content/extensions/helpers/template/index.md +6 -3
- package/content/extensions/helpers/types/index.md +11 -8
- package/content/extensions/helpers/websocket/api.md +9 -9
- package/content/extensions/helpers/websocket/index.md +7 -7
- package/content/extensions/helpers/worker-thread/index.md +2 -2
- package/content/extensions/index.md +3 -4
- package/content/extensions/src-details/mcp-server.md +18 -24
- package/content/guides/core-concepts/application/bootstrapping.md +11 -14
- package/content/guides/core-concepts/application/index.md +3 -3
- package/content/guides/core-concepts/components.md +19 -10
- package/content/guides/core-concepts/dependency-injection.md +6 -3
- package/content/guides/core-concepts/grpc-controllers.md +6 -5
- package/content/guides/core-concepts/persistent/datasources.md +42 -43
- package/content/guides/core-concepts/persistent/index.md +16 -7
- package/content/guides/core-concepts/persistent/models.md +24 -20
- package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
- package/content/guides/core-concepts/persistent/repositories.md +40 -23
- package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
- package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
- package/content/guides/core-concepts/persistent/transactions.md +61 -25
- package/content/guides/core-concepts/rest-controllers.md +12 -9
- package/content/guides/core-concepts/services.md +330 -60
- package/content/guides/get-started/5-minute-quickstart.md +15 -15
- package/content/guides/get-started/philosophy.md +36 -36
- package/content/guides/get-started/setup.md +3 -3
- package/content/guides/index.md +3 -3
- package/content/guides/migrations/redis-helpers-migration.md +177 -0
- package/content/guides/migrations/scoped-rbac-migration.md +17 -17
- package/content/guides/migrations/unified-connectors-migration.md +113 -0
- package/content/guides/reference/glossary.md +19 -12
- package/content/guides/reference/mcp-docs-server.md +22 -18
- package/content/guides/tutorials/building-a-crud-api.md +37 -44
- package/content/guides/tutorials/complete-installation.md +17 -17
- package/content/guides/tutorials/ecommerce-api.md +163 -124
- package/content/guides/tutorials/realtime-chat.md +181 -135
- package/content/guides/tutorials/testing.md +65 -523
- package/content/index.md +2 -180
- package/content/public/apple-touch-icon.png +0 -0
- package/content/public/og-image.png +0 -0
- package/content/public/site.webmanifest +11 -0
- package/content/references/base/application.md +4 -5
- package/content/references/base/bootstrapping.md +18 -5
- package/content/references/base/components.md +149 -120
- package/content/references/base/connectors.md +178 -0
- package/content/references/base/controllers.md +41 -30
- package/content/references/base/datasources.md +163 -92
- package/content/references/base/dependency-injection.md +34 -22
- package/content/references/base/filter-system/application-usage.md +17 -14
- package/content/references/base/filter-system/array-operators.md +7 -2
- package/content/references/base/filter-system/comparison-operators.md +3 -0
- package/content/references/base/filter-system/default-filter.md +89 -71
- package/content/references/base/filter-system/fields-order-pagination.md +22 -22
- package/content/references/base/filter-system/index.md +6 -3
- package/content/references/base/filter-system/json-filtering.md +20 -1
- package/content/references/base/filter-system/list-operators.md +1 -1
- package/content/references/base/filter-system/logical-operators.md +33 -1
- package/content/references/base/filter-system/null-operators.md +30 -1
- package/content/references/base/filter-system/quick-reference.md +23 -4
- package/content/references/base/filter-system/tips.md +5 -5
- package/content/references/base/filter-system/use-cases.md +12 -12
- package/content/references/base/grpc-controllers.md +13 -13
- package/content/references/base/index.md +24 -12
- package/content/references/base/middlewares.md +265 -327
- package/content/references/base/models.md +63 -49
- package/content/references/base/providers.md +136 -130
- package/content/references/base/repositories/advanced.md +59 -58
- package/content/references/base/repositories/index.md +115 -91
- package/content/references/base/repositories/mixins.md +55 -291
- package/content/references/base/repositories/relations.md +54 -64
- package/content/references/base/repositories/soft-deletable.md +31 -30
- package/content/references/base/services.md +296 -93
- package/content/references/configuration/environment-variables.md +49 -31
- package/content/references/configuration/index.md +6 -6
- package/content/references/index.md +17 -12
- package/content/references/quick-reference.md +65 -106
- package/content/references/utilities/crypto.md +65 -23
- package/content/references/utilities/index.md +3 -3
- package/content/references/utilities/jsx.md +6 -4
- package/content/references/utilities/module.md +68 -20
- package/content/references/utilities/parse.md +4 -14
- package/content/references/utilities/promise.md +9 -7
- package/content/references/utilities/schema.md +5 -3
- package/dist/mcp-server/common/guards.d.ts +8 -0
- package/dist/mcp-server/common/guards.d.ts.map +1 -0
- package/dist/mcp-server/common/guards.js +14 -0
- package/dist/mcp-server/common/guards.js.map +1 -0
- package/dist/mcp-server/common/index.d.ts +1 -0
- package/dist/mcp-server/common/index.d.ts.map +1 -1
- package/dist/mcp-server/common/index.js +1 -0
- package/dist/mcp-server/common/index.js.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +4 -2
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/helpers/github.helper.js +1 -1
- package/dist/mcp-server/index.js +7 -2
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +6 -2
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/base.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
- package/package.json +9 -9
- package/content/extensions/helpers/testing/index.md +0 -510
- 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
|
|
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)
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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)**
|
|
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)**
|
|
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
|
|
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
|
|
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 = _, _, _`
|
|
807
|
-
- `g(r.sub, p.sub, r.dom)`
|
|
808
|
-
- `keyMatch(r.dom, p.dom)`
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)`
|
|
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
|
|
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
|
|
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
|
|
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](./
|
|
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/) -
|
|
84
|
-
- [Swagger](./
|
|
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 | `
|
|
14
|
-
| `subject` is falsy | 400 | `
|
|
15
|
-
| Both `text` and `html` are falsy | 400 | `
|
|
16
|
-
| Transport throws during `send()` | 500 | `
|
|
17
|
-
| Batch operation fails | 500 | `
|
|
18
|
-
| Template engine not configured for `sendTemplate()` | 500 | `
|
|
19
|
-
| Transport throws during `verify()` | 500 | `
|
|
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 | `
|
|
32
|
-
| Nodemailer options fail type guard | 500 | `
|
|
33
|
-
| Mailgun options fail type guard | 500 | `
|
|
34
|
-
| Custom options fail type guard | 500 | `
|
|
35
|
-
| Custom config missing `send`/`verify` | 500 | `
|
|
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 | `
|
|
51
|
-
| Missing template data keys (with `requireValidate: true`) | 400 | `
|
|
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
|
-
### "
|
|
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: '
|
|
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
|
|
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
|
|
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:
|
|
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 `
|
|
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
|
|
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 `
|
|
684
|
+
The server helper creates 3 independent Redis connections from a single `AbstractRedisHelper` instance:
|
|
685
685
|
|
|
686
686
|
```
|
|
687
|
-
|
|
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 `
|
|
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:
|
|
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:
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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:
|
|
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
|
|
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<
|
|
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 `
|
|
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
|
|
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` | `
|
|
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:
|
|
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
|
|
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
|
|
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` |
|
|
218
|
-
| `modified_at` |
|
|
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
|
-
|
|
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:**
|
|
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
|