@venizia/ignis-docs 0.2.0 → 0.2.1-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 (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -1,46 +1,178 @@
1
- # Static Asset -- API Reference
1
+ ---
2
+ title: Static Asset Component - Full Reference
3
+ description: Controller factory, IStorageHelper interface, storage helper options, MetaLink schema, and per-endpoint request/response reference
4
+ difficulty: intermediate
5
+ ---
6
+
7
+ # Static Asset Component Reference
8
+
9
+ Every binding, endpoint, type, and internal mechanism of `StaticAssetComponent`. For task-oriented walkthroughs, see [Usage & Examples](./usage).
10
+
11
+ **Files:**
12
+
13
+ - [`packages/core/src/components/static-asset/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/component.ts)
14
+ - [`packages/core/src/components/static-asset/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/common/types.ts)
15
+ - [`packages/core/src/components/static-asset/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/common/constants.ts)
16
+ - [`packages/core/src/components/static-asset/common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/common/keys.ts)
17
+ - [`packages/core/src/components/static-asset/controller/factory.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/controller/factory.ts)
18
+ - [`packages/core/src/components/static-asset/controller/base.definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/controller/base.definition.ts)
19
+ - [`packages/core/src/components/static-asset/models/base.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/models/base.model.ts)
20
+ - [`packages/core/src/components/static-asset/repositories/base.repository.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/repositories/base.repository.ts)
21
+ - [`packages/helpers/src/modules/storage/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/storage/base.ts)
22
+ - [`packages/helpers/src/modules/storage/disk/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/storage/disk/helper.ts)
23
+ - [`packages/helpers/src/modules/storage/minio/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/storage/minio/helper.ts)
24
+ - [`packages/helpers/src/modules/storage/bun-s3/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/storage/bun-s3/helper.ts)
25
+ - [`packages/helpers/src/utilities/request.utility.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/utilities/request.utility.ts)
26
+
27
+ ## Quick reference
28
+
29
+ | Item | Value |
30
+ |------|-------|
31
+ | Package | `@venizia/ignis` (core component) + `@venizia/ignis-helpers` (storage helpers) |
32
+ | Component class | `StaticAssetComponent` |
33
+ | Import subpath | `@venizia/ignis/static-asset` - not on the root barrel |
34
+ | Storage helpers | `DiskHelper`, `MinioHelper` (`@venizia/ignis-helpers/minio`), `BunS3Helper` (`@venizia/ignis-helpers/bun-s3`) |
35
+ | Runtimes | Both - `BunS3Helper` specifically requires Bun (imports Bun's native `S3Client`) |
36
+ | Optional feature | MetaLink - Postgres-backed upload tracking via `BaseMetaLinkModel`/`BaseMetaLinkRepository` |
37
+
38
+ ## Import paths
2
39
 
3
- > Controller factory, storage interface, type definitions, and component internals.
40
+ ```typescript
41
+ // Core - subpath import only
42
+ import {
43
+ StaticAssetComponent,
44
+ StaticAssetComponentBindingKeys,
45
+ StaticAssetStorageTypes,
46
+ AssetControllerFactory,
47
+ BaseMetaLinkModel,
48
+ BaseMetaLinkRepository,
49
+ } from '@venizia/ignis/static-asset';
50
+
51
+ import type {
52
+ TStaticAssetsComponentOptions,
53
+ TStaticAssetExtraOptions,
54
+ TMetaLinkConfig,
55
+ TStaticAssetStorageType,
56
+ IAssetControllerOptions,
57
+ } from '@venizia/ignis/static-asset';
58
+
59
+ // Helpers - main entry + storage-backend subpaths
60
+ import { DiskHelper } from '@venizia/ignis-helpers';
61
+ import { MinioHelper } from '@venizia/ignis-helpers/minio';
62
+ import { BunS3Helper } from '@venizia/ignis-helpers/bun-s3';
63
+ ```
64
+
65
+ ## Binding keys
4
66
 
5
- ## Controller Factory
67
+ | Key | Constant | Type | Required | Default |
68
+ |-----|----------|------|----------|---------|
69
+ | `@app/static-asset-component/options` | `StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS` | `TStaticAssetsComponentOptions` | Yes | `{}` |
6
70
 
7
- The `AssetControllerFactory.defineAssetController()` method dynamically creates controller classes at runtime. For each storage backend in the options:
71
+ > [!NOTE]
72
+ > `StaticAssetComponent`'s constructor binds an empty `{}` default for this key. `binding()` iterates `Object.entries(componentOptions)` - an empty object produces zero controllers, no error. Bind your configuration in `preConfigure()` before `this.component(StaticAssetComponent)`.
8
73
 
9
- 1. A new class extending `BaseRestController` is created with `@controller({ path: basePath })`
10
- 2. The class name is set dynamically via `Object.defineProperty(_controller, 'name', { value: name })`
11
- 3. Routes are bound in the controller's `binding()` method using `this.bindRoute().to()`
12
- 4. Route configs are spread-merged with per-route overrides from `controller.routes` (e.g., `{ ...StaticAssetDefinitions.UPLOAD, ...routes?.upload }`)
13
- 5. The controller is registered via `this.application.controller()`
74
+ ## `TStaticAssetsComponentOptions`
14
75
 
76
+ ```typescript
77
+ type TStaticAssetsComponentOptions = {
78
+ [key: string]: {
79
+ controller: {
80
+ name: string;
81
+ basePath: string;
82
+ isStrict?: boolean;
83
+ routes?: {
84
+ getBuckets?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
85
+ getBucketByName?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
86
+ createBucket?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
87
+ deleteBucket?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
88
+ upload?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
89
+ listObjects?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
90
+ deleteObject?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
91
+ getObjectByName?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
92
+ downloadObjectByName?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
93
+ recreateMetaLink?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
94
+ };
95
+ };
96
+ extra?: TStaticAssetExtraOptions;
97
+ } & (
98
+ | { storage: typeof StaticAssetStorageTypes.BUN_S3; helper: BunS3Helper }
99
+ | { storage: typeof StaticAssetStorageTypes.DISK; helper: DiskHelper }
100
+ | { storage: typeof StaticAssetStorageTypes.MINIO; helper: MinioHelper }
101
+ ) &
102
+ ({ useMetaLink?: false | undefined } | { useMetaLink: true; metaLink: TMetaLinkConfig });
103
+ };
15
104
  ```
16
- StaticAssetComponent.binding()
17
- | iterates options
18
- AssetControllerFactory.defineAssetController({ controller, storage, helper, ... })
19
- | creates
20
- @controller({ path: basePath })
21
- class _controller extends BaseRestController { ... }
22
- | registered via
23
- this.application.controller(_controller)
105
+
106
+ | Field | Type | Default | Description |
107
+ |-------|------|---------|--------------|
108
+ | `controller.name` | `string` | - | Class name given to the generated controller (via `Object.defineProperty`) |
109
+ | `controller.basePath` | `string` | - | Mount path, e.g. `'/assets'` |
110
+ | `controller.isStrict` | `boolean` | `true` | Passed through to `BaseRestController`'s strict routing mode |
111
+ | `controller.routes` | object | `undefined` | Per-route overrides - see [Per-route overrides](#per-route-overrides) |
112
+ | `storage` | `'disk' \| 'minio' \| 'bun-s3'` | - | Selects which `helper` type is required (discriminated union) |
113
+ | `helper` | `DiskHelper \| MinioHelper \| BunS3Helper` | - | Storage backend instance matching `storage` |
114
+ | `extra` | `TStaticAssetExtraOptions` | `undefined` | Multipart parsing mode, name/link normalization, max folder depth |
115
+ | `useMetaLink` | `boolean` | `false` | Enables the `PUT .../meta-links/:objectName` route and DB tracking on upload/delete |
116
+ | `metaLink` | `TMetaLinkConfig` | - | Required when `useMetaLink: true`; ignored otherwise |
117
+
118
+ ### Per-route overrides
119
+
120
+ Each key accepts a `Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>` - typically `authenticate`, `authorize`, `path`, and `middleware`. It is shallow-merged onto the base definition: `{ ...StaticAssetDefinitions.UPLOAD, ...routes?.upload }`.
121
+
122
+ | Route key | HTTP Method | Base Path |
123
+ |-----------|-------------|-----------|
124
+ | `getBuckets` | `GET` | `/buckets` |
125
+ | `getBucketByName` | `GET` | <code v-pre>/buckets/{bucketName}</code> |
126
+ | `createBucket` | `POST` | <code v-pre>/buckets/{bucketName}</code> |
127
+ | `deleteBucket` | `DELETE` | <code v-pre>/buckets/{bucketName}</code> |
128
+ | `upload` | `POST` | <code v-pre>/buckets/{bucketName}/upload</code> |
129
+ | `listObjects` | `GET` | <code v-pre>/buckets/{bucketName}/objects</code> |
130
+ | `getObjectByName` | `GET` | <code v-pre>/buckets/{bucketName}/objects/{objectName}</code> |
131
+ | `downloadObjectByName` | `GET` | <code v-pre>/buckets/{bucketName}/download/{objectName}</code> |
132
+ | `deleteObject` | `DELETE` | <code v-pre>/buckets/{bucketName}/objects/{objectName}</code> |
133
+ | `recreateMetaLink` | `PUT` | <code v-pre>/buckets/{bucketName}/meta-links/{objectName}</code> - only registered when `useMetaLink: true` |
134
+
135
+ ## `TStaticAssetExtraOptions`
136
+
137
+ ```typescript
138
+ type TStaticAssetExtraOptions = {
139
+ parseMultipartBody?: {
140
+ storage?: 'memory' | 'disk';
141
+ uploadDir?: string;
142
+ };
143
+ normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
144
+ normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
145
+ /** Maximum folder nesting depth allowed in object paths. Default: 2 */
146
+ maxFolderDepth?: number;
147
+ [key: string]: any;
148
+ };
24
149
  ```
25
150
 
26
- ### IAssetControllerOptions
151
+ > [!NOTE]
152
+ > `normalizeNameFn` receives **both** `originalName` and `folderPath` - the second lets a custom implementation decide how to fold the target folder into the stored name. Omit `folderPath` handling and nested uploads flatten into the bucket root.
153
+
154
+ | Field | Default | Notes |
155
+ |-------|---------|-------|
156
+ | `parseMultipartBody.storage` | `'memory'` | `'disk'` spools to `uploadDir`, then the controller reads the file back with `readFileSync` before handing it to the storage helper |
157
+ | `parseMultipartBody.uploadDir` | `'./uploads'` | Created with <code v-pre>fs.mkdirSync({ recursive: true })</code> if missing |
158
+ | `normalizeNameFn` | `BaseStorageHelper`'s internal lowercase + `_`-for-space normalizer | Runs before the file is written; its output is re-validated with `isValidPath()` |
159
+ | `normalizeLinkFn` | Component-generated - see below | Runs after the write to build the returned `link` |
160
+ | `maxFolderDepth` | `BaseStorageHelper.DEFAULT_MAX_FOLDER_DEPTH` (`2`) | Folder segments only - the filename itself does not count against this limit |
161
+
162
+ ### Default `normalizeLinkFn`
27
163
 
28
- The factory method accepts the following options:
164
+ `StaticAssetComponent.binding()` always supplies a `normalizeLinkFn` to the factory - your own `extra.normalizeLinkFn` if set, otherwise this default:
29
165
 
30
166
  ```typescript
31
- interface IAssetControllerOptions {
32
- controller: TStaticAssetsComponentOptions[string]['controller'];
33
- storage: TStaticAssetStorageType;
34
- helper: IStorageHelper;
35
- useMetaLink?: boolean;
36
- metaLink?: TMetaLinkConfig;
37
- options?: TStaticAssetExtraOptions;
38
- }
167
+ (opts: { bucketName: string; normalizeName: string }) => {
168
+ const encodedPath = encodeURIComponent(opts.normalizeName);
169
+ return `${controller.basePath}/buckets/${opts.bucketName}/objects/${encodedPath}`;
170
+ };
39
171
  ```
40
172
 
41
- ## StaticAssetStorageTypes
173
+ This is why every generated link points back at the `objects/{objectName}` stream route by default, regardless of storage backend. `BaseStorageHelper`'s own backend-specific `normalizeObjectLink()` (used when a helper's `upload()` is called directly, outside the component) is never reached through `StaticAssetComponent` - the component's default always takes priority when no `normalizeLinkFn` is set.
42
174
 
43
- A constants class following the IGNIS pattern with `static readonly` fields, a `SCHEME_SET`, and an `isValid()` method:
175
+ ## Storage types
44
176
 
45
177
  ```typescript
46
178
  class StaticAssetStorageTypes {
@@ -56,95 +188,38 @@ class StaticAssetStorageTypes {
56
188
  }
57
189
 
58
190
  type TStaticAssetStorageType = TConstValue<typeof StaticAssetStorageTypes>;
59
- // Resolves to: 'disk' | 'minio' | 'bun-s3'
60
- ```
61
-
62
- ## MultipartBodySchema
63
-
64
- The Zod schema used to validate the upload request body:
65
-
66
- ```typescript
67
- const MultipartBodySchema = z.object({
68
- files: z.union([z.instanceof(File), z.array(z.instanceof(File))]).openapi({
69
- type: 'array',
70
- items: {
71
- type: 'string',
72
- format: 'binary',
73
- },
74
- }),
75
- });
76
- ```
77
-
78
- This accepts either a single `File` or an array of `File` objects. The OpenAPI spec representation uses `type: 'array'` with `format: 'binary'` items for compatibility with Swagger/OpenAPI tooling.
79
-
80
- ## Header Sanitization
81
-
82
- When streaming files (both inline and download), the controller forwards a specific set of whitelisted headers from the storage metadata to the response. All other metadata headers are dropped.
83
-
84
- ### WHITELIST_HEADERS
85
-
86
- The exact list of forwarded headers:
87
-
88
- ```typescript
89
- const WHITELIST_HEADERS = [
90
- 'content-type',
91
- 'content-encoding',
92
- 'cache-control',
93
- 'etag',
94
- 'last-modified',
95
- ] as const;
96
- ```
97
-
98
- These correspond to `HTTP.Headers.CONTENT_TYPE`, `HTTP.Headers.CONTENT_ENCODING`, `HTTP.Headers.CACHE_CONTROL`, `HTTP.Headers.ETAG`, and `HTTP.Headers.LAST_MODIFIED` from `@venizia/ignis-helpers`.
99
-
100
- All header values are sanitized by stripping `\r` and `\n` characters via `String(value).replace(/[\r\n]/g, '')` to prevent HTTP header injection attacks. If no `content-type` header is present in the storage metadata, the controller falls back to `application/octet-stream`.
101
-
102
- ### HTTP Security Headers
103
-
104
- All file streaming responses include:
105
-
106
- ```http
107
- X-Content-Type-Options: nosniff
108
- Content-Type: <from metadata or application/octet-stream>
109
- Content-Length: <file size in bytes>
110
- Content-Disposition: attachment; filename="..." (download endpoint only)
191
+ // 'disk' | 'minio' | 'bun-s3'
111
192
  ```
112
193
 
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
-
115
- ## Object Name Decoding
194
+ | Type | Constant | Helper | Requires |
195
+ |------|----------|--------|----------|
196
+ | `'disk'` | `StaticAssetStorageTypes.DISK` | `DiskHelper` | Local filesystem write access |
197
+ | `'minio'` | `StaticAssetStorageTypes.MINIO` | `MinioHelper` | A MinIO or S3-compatible endpoint |
198
+ | `'bun-s3'` | `StaticAssetStorageTypes.BUN_S3` | `BunS3Helper` | Bun runtime (imports Bun's native `S3Client`) |
116
199
 
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.
200
+ ## Storage helpers
118
201
 
119
- A prior second `decodeURIComponent()` was actively wrong:
202
+ ### `IStorageHelper` interface
120
203
 
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
-
126
- ## IStorageHelper Interface
127
-
128
- All storage helpers implement this unified interface:
204
+ Every backend implements this contract; `BaseStorageHelper` (abstract) implements the shared parts (`isValidName`, `isValidPath`, `upload`, `getMimeType`, `getFileType`) and leaves the rest abstract.
129
205
 
130
206
  ```typescript
131
207
  interface IStorageHelper {
132
208
  isValidName(name: string): boolean;
133
209
  isValidPath(pathStr: string, opts?: { maxDepth?: number }): boolean;
134
210
 
135
- // Bucket operations
136
211
  isBucketExists(opts: { name: string }): Promise<boolean>;
137
212
  getBuckets(): Promise<IBucketInfo[]>;
138
213
  getBucket(opts: { name: string }): Promise<IBucketInfo | null>;
139
214
  createBucket(opts: { name: string }): Promise<IBucketInfo | null>;
140
215
  removeBucket(opts: { name: string }): Promise<boolean>;
141
216
 
142
- // File operations
143
217
  upload(opts: {
144
218
  bucket: string;
145
219
  files: IUploadFile[];
146
220
  normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
147
221
  normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
222
+ maxFolderDepth?: number;
148
223
  }): Promise<IUploadResult[]>;
149
224
 
150
225
  getFile(opts: { bucket: string; name: string; options?: any }): Promise<Readable>;
@@ -153,12 +228,21 @@ interface IStorageHelper {
153
228
  removeObjects(opts: { bucket: string; names: string[] }): Promise<void>;
154
229
  listObjects(opts: IListObjectsOptions): Promise<IObjectInfo[]>;
155
230
 
156
- // Utility
157
231
  getFileType(opts: { mimeType: string }): string;
158
232
  }
159
233
  ```
160
234
 
161
- ### Supporting Types
235
+ ```
236
+ IStorageHelper (interface)
237
+ |
238
+ BaseStorageHelper (abstract - implements isValidName/isValidPath/upload/getMimeType/getFileType)
239
+ |
240
+ +-- DiskHelper (local filesystem)
241
+ +-- MinioHelper (S3-compatible)
242
+ +-- BunS3Helper (Bun-native S3, Bun only)
243
+ ```
244
+
245
+ ### Supporting types
162
246
 
163
247
  ```typescript
164
248
  interface IUploadFile {
@@ -208,66 +292,250 @@ interface IListObjectsOptions {
208
292
  }
209
293
  ```
210
294
 
211
- ### Storage Helper Hierarchy
295
+ ### Name and path validation (`BaseStorageHelper`)
296
+
297
+ `isValidName(name)` rejects, in order: non-string input, empty string, `..`/`/`/`\` (path traversal), a leading `.` (hidden file), any of `;`, `\|`, `&`, `$`, `` ` ``, `<`, `>`, `{`, `}`, `[`, `]`, `!`, `#` (shell metacharacters), `\n`/`\r`/`\0` (control characters), length over 255, and whitespace-only input.
298
+
299
+ `isValidPath(pathStr, { maxDepth })` trims leading/trailing slashes, rejects an empty result, rejects double slashes (empty segments), computes `folderDepth = segments.length - 1` and rejects it exceeding `maxDepth` (default `BaseStorageHelper.DEFAULT_MAX_FOLDER_DEPTH = 2`), validates every segment through `isValidName()`, and rejects a normalized length over 1024 characters.
212
300
 
301
+ ### `DiskHelper`
302
+
303
+ ```typescript
304
+ interface IDiskHelperOptions {
305
+ basePath: string; // Base directory for storage
306
+ scope?: string; // Logger scope, default: 'DiskHelper'
307
+ identifier?: string; // Helper identifier, default: 'DiskHelper'
308
+ }
213
309
  ```
214
- IStorageHelper (interface)
215
- |
216
- BaseStorageHelper (abstract class)
217
- |
218
- +-- DiskHelper (local filesystem)
219
- +-- MinioHelper (S3-compatible)
220
- +-- BunS3Helper (Bun-native S3)
310
+
311
+ Creates `basePath` with `fs.mkdirSync({ recursive: true })` in the constructor if it does not already exist. Buckets map to subdirectories; objects map to files inside them.
312
+
313
+ ```typescript
314
+ const diskHelper = new DiskHelper({ basePath: './app_data/storage' });
315
+ ```
316
+
317
+ ### `MinioHelper`
318
+
319
+ ```typescript
320
+ interface IMinioHelperOptions extends ClientOptions { // minio's own SDK options, plus:
321
+ scope?: string;
322
+ identifier?: string;
323
+ }
221
324
  ```
222
325
 
223
- ## MetaLink SQL Schema
326
+ `ClientOptions` comes straight from the `minio` package - `endPoint`, `port`, `useSSL`, `accessKey`, `secretKey`, and the rest of the MinIO client's own configuration surface.
327
+
328
+ ```typescript
329
+ const minioHelper = new MinioHelper({
330
+ endPoint: 'minio.example.com',
331
+ port: 9000,
332
+ useSSL: true,
333
+ accessKey: process.env.MINIO_ACCESS_KEY,
334
+ secretKey: process.env.MINIO_SECRET_KEY,
335
+ });
336
+ ```
337
+
338
+ ### `BunS3Helper`
339
+
340
+ ```typescript
341
+ interface IBunS3HelperOptions {
342
+ accessKey: string;
343
+ secretKey: string;
344
+ endpoint: string;
345
+ region?: string; // Default: 'us-east-1'
346
+ sessionToken?: string;
347
+ scope?: string;
348
+ identifier?: string;
349
+ }
350
+ ```
351
+
352
+ Wraps Bun's native `S3Client`, imported from the `bun` builtin module - it only resolves under the Bun runtime, which is why it is exported from the separate `@venizia/ignis-helpers/bun-s3` subpath rather than the main entry point.
353
+
354
+ ```typescript
355
+ import { BunS3Helper } from '@venizia/ignis-helpers/bun-s3';
356
+
357
+ const bunS3Helper = new BunS3Helper({
358
+ accessKey: process.env.S3_ACCESS_KEY,
359
+ secretKey: process.env.S3_SECRET_KEY,
360
+ endpoint: 'https://s3.us-east-1.amazonaws.com',
361
+ });
362
+ ```
363
+
364
+ ## Controller factory
365
+
366
+ `AssetControllerFactory.defineAssetController(opts: IAssetControllerOptions)` builds one controller class per call:
367
+
368
+ ```typescript
369
+ interface IAssetControllerOptions {
370
+ controller: TStaticAssetsComponentOptions[string]['controller'];
371
+ storage: TStaticAssetStorageType;
372
+ helper: IStorageHelper;
373
+ useMetaLink?: boolean;
374
+ metaLink?: TMetaLinkConfig;
375
+ options?: TStaticAssetExtraOptions;
376
+ }
377
+ ```
378
+
379
+ 1. Creates a class extending `BaseRestController`, decorated `@controller({ path: basePath })`.
380
+ 2. Renames it via `Object.defineProperty(GeneratedStaticAssetController, 'name', { value: name, configurable: true })` so logs and DI bindings show your configured `controller.name`, not a generic factory name.
381
+ 3. Binds every route in `binding()` with `this.bindRoute({ configs }).to({ handler })`, spread-merging each base definition with its `routes?.<key>` override.
382
+ 4. Registers `recreateMetaLink` only when `useMetaLink && metaLink` are both set.
383
+ 5. `StaticAssetComponent.binding()` registers the resulting class with `this.application.controller(...)`.
384
+
385
+ ```
386
+ StaticAssetComponent.binding()
387
+ | iterates componentOptions
388
+ AssetControllerFactory.defineAssetController({ controller, storage, helper, ... })
389
+ | creates
390
+ @controller({ path: basePath })
391
+ class GeneratedStaticAssetController extends BaseRestController { ... }
392
+ | registered via
393
+ this.application.controller(GeneratedStaticAssetController)
394
+ ```
395
+
396
+ ### `MultipartBodySchema`
397
+
398
+ The Zod schema validating the `upload` request body:
399
+
400
+ ```typescript
401
+ const MultipartBodySchema = z.object({
402
+ files: z.union([z.instanceof(File), z.array(z.instanceof(File))]).openapi({
403
+ type: 'array',
404
+ items: { type: 'string', format: 'binary' },
405
+ }),
406
+ });
407
+ ```
408
+
409
+ ### Endpoint reference
410
+
411
+ | Method | Path | Notes |
412
+ |--------|------|-------|
413
+ | `GET` | `/buckets` | No params. Returns `IBucketInfo[]` |
414
+ | `GET` | <code v-pre>/buckets/{bucketName}</code> | Returns `IBucketInfo \| null` |
415
+ | `POST` | <code v-pre>/buckets/{bucketName}</code> | Returns `IBucketInfo \| null` (`null` if creation failed, e.g. already exists) |
416
+ | `DELETE` | <code v-pre>/buckets/{bucketName}</code> | Returns <code v-pre>{ isDeleted: boolean }</code> |
417
+ | `POST` | <code v-pre>/buckets/{bucketName}/upload</code> | `multipart/form-data` body; query: `principalType?`, `principalId?`, `variant?`, `folderPath?`. Returns `IUploadResult[]` |
418
+ | `GET` | <code v-pre>/buckets/{bucketName}/objects</code> | Query: `prefix?`, `recursive?` (`'true'` string only), `maxKeys?` (positive integer string). Returns `IObjectInfo[]` |
419
+ | `GET` | <code v-pre>/buckets/{bucketName}/objects/{objectName}</code> | Streams the file inline. `objectName` is a single percent-encoded segment |
420
+ | `GET` | <code v-pre>/buckets/{bucketName}/download/{objectName}</code> | Streams the file with `Content-Disposition: attachment` |
421
+ | `DELETE` | <code v-pre>/buckets/{bucketName}/objects/{objectName}</code> | Returns <code v-pre>{ success: boolean }</code> |
422
+ | `PUT` | <code v-pre>/buckets/{bucketName}/meta-links/{objectName}</code> | Only registered when `useMetaLink: true`. Returns <code v-pre>{ success: boolean, metaLink }</code> |
423
+
424
+ ### Upload validation order
425
+
426
+ 1. `bucketName` validated with `isValidName()` - `400 "Invalid bucket name"` on failure.
427
+ 2. If `folderPath` is present: trimmed of leading/trailing slashes, `400 "Invalid folder path"` if empty after trimming; segment count checked against `maxFolderDepth`, `400 "Folder path exceeds max depth of {n}"` if over; each segment checked with `isValidName()`, `400 "Invalid folder path segment: {segment}"` if any fails.
428
+ 3. `multipart/form-data` parsed via `parseMultipartBody()`.
429
+ 4. Each file's effective buffer (direct `buffer`, or `readFileSync(file.path)` when `storage: 'disk'` was used) checked non-empty - `400 "Empty file content | name: {originalName}"` if empty.
430
+ 5. `helper.upload()` runs the storage-helper-level checks below.
431
+ 6. Spool files written by `storage: 'disk'` parsing are removed in a `finally` block via `rmSync({ force: true })`, regardless of success or failure; removal errors are logged, never thrown.
432
+
433
+ ### Storage-helper-level upload checks (`BaseStorageHelper.upload`)
434
+
435
+ These run inside `helper.upload()`, independent of and in addition to the controller's own checks above - reachable even when a caller uses the storage helper directly:
436
+
437
+ | Check | Error message | Default status |
438
+ |-------|----------------|-----------------|
439
+ | Bucket does not exist (`isBucketExists()` false) | <code v-pre>[upload] Bucket does not exist \| name: {bucket}</code> | `400` |
440
+ | `originalName` fails `isValidName()` | `[upload] Invalid original file name` | `400` |
441
+ | `folderPath` exceeds depth or fails `isValidPath()` | `[upload] Invalid folder path` | `400` |
442
+ | `size` is `undefined`, `null`, or negative | <code v-pre>[upload] Invalid file size \| size: {size}</code> | `400` |
443
+ | Normalized name (post `normalizeNameFn`) fails `isValidPath()` | <code v-pre>[upload] Invalid normalized object name \| name: {name}</code> | `400` |
444
+
445
+ `getError()` defaults `statusCode` to `400` when the caller does not pass one explicitly - every message above is thrown without an explicit status, so all resolve to `400`.
446
+
447
+ ## Header sanitization
448
+
449
+ ```typescript
450
+ const WHITELIST_HEADERS = [
451
+ 'content-type',
452
+ 'content-encoding',
453
+ 'cache-control',
454
+ 'etag',
455
+ 'last-modified',
456
+ ] as const;
457
+ ```
458
+
459
+ These correspond to `HTTP.Headers.CONTENT_TYPE`, `CONTENT_ENCODING`, `CACHE_CONTROL`, `ETAG`, and `LAST_MODIFIED` from `@venizia/ignis-helpers`. When streaming a file (both `objects/{objectName}` and `download/{objectName}`), the controller copies only these keys from the storage metadata onto the response; every other metadata header is dropped. Each forwarded value is sanitized with `String(value).replace(/[\r\n]/g, '')` before being set, to prevent HTTP header injection.
460
+
461
+ All streaming responses also set:
462
+
463
+ ```http
464
+ X-Content-Type-Options: nosniff
465
+ Content-Type: <from metadata, or application/octet-stream as fallback>
466
+ Content-Length: <file size in bytes>
467
+ Content-Disposition: attachment; filename="..." (download endpoint only, via createContentDispositionHeader())
468
+ ```
469
+
470
+ ## Object name decoding
471
+
472
+ Hono percent-decodes a path param before the handler reads it. The controller's `readObjectName()` is therefore a deliberate no-op - it does not run a second `decodeURIComponent()`:
473
+
474
+ - `report_100%.pdf` is a legal object name. Its link is `.../objects/report_100%25.pdf`; Hono hands the handler back `report_100%.pdf`. A second decode would hit the invalid escape `%.p` and throw, making the object permanently unfetchable and undeletable.
475
+ - An object named `a%2Fb.png` would decode twice into `a/b.png` - a different object than the one requested.
476
+
477
+ `isValidName()`/`isValidPath()` still run on the singly-decoded value, so a traversal payload is rejected exactly as before.
478
+
479
+ ## `TMetaLinkConfig`
480
+
481
+ ```typescript
482
+ type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
483
+ model: typeof BaseRelationalEntity<Schema>;
484
+ repository: DefaultRelationalRepository<Schema>;
485
+ createMetaLink?: (opts: {
486
+ uploadResult: IUploadResult;
487
+ fileStat: IFileStat;
488
+ query: TUploadQuery;
489
+ }) => ValueOrPromise<{ count: number; data: Schema }>;
490
+ };
491
+ ```
492
+
493
+ `BaseRelationalEntity` and `DefaultRelationalRepository` are the canonical class names (`packages/core/src/connectors/postgres/`); `BasePostgresEntity` and `DefaultCRUDRepository` are re-exported aliases of the same classes.
494
+
495
+ ## MetaLink SQL schema
224
496
 
225
497
  **Table:** `MetaLink`
226
498
 
227
499
  | Field | Type | Nullable | Default | Description |
228
500
  |-------|------|----------|---------|-------------|
229
- | `id` | TEXT | No | -- | Primary key (UUID) |
230
- | `created_at` | TIMESTAMPTZ | No | `NOW()` | When record was created |
231
- | `modified_at` | TIMESTAMPTZ | No | `NOW()` | When record was last updated |
232
- | `bucket_name` | TEXT | No | -- | Storage bucket name |
233
- | `object_name` | TEXT | No | -- | File object name |
234
- | `link` | TEXT | No | -- | Access URL to the file |
235
- | `mimetype` | TEXT | No | -- | File MIME type |
236
- | `size` | INTEGER | No | -- | File size in bytes |
237
- | `etag` | TEXT | Yes | -- | Entity tag for versioning |
238
- | `metadata` | JSONB | Yes | -- | Additional file metadata |
239
- | `storage_type` | TEXT | No | -- | Storage type (`'disk'`, `'minio'`, or `'bun-s3'`) |
240
- | `is_synced` | BOOLEAN | No | `false` | Whether MetaLink is synchronized with storage |
241
- | `variant` | TEXT | Yes | -- | Upload variant tag (e.g., `'thumbnail'`, `'original'`) |
242
- | `principal_type` | TEXT | Yes | -- | Type of the associated principal (e.g., `'user'`, `'service'`) |
243
- | `principal_id` | TEXT | Yes | -- | ID of the associated principal (always stored as string) |
501
+ | `id` | TEXT | No | - | Primary key |
502
+ | `created_at` | TIMESTAMPTZ | No | `NOW()` | Row creation time |
503
+ | `modified_at` | TIMESTAMPTZ | No | `NOW()` | Row last-update time |
504
+ | `bucket_name` | TEXT | No | - | Storage bucket name |
505
+ | `object_name` | TEXT | No | - | File object name (may include folder segments) |
506
+ | `link` | TEXT | No | - | Access URL to the file |
507
+ | `mimetype` | TEXT | No | - | File MIME type |
508
+ | `size` | INTEGER | No | - | File size in bytes |
509
+ | `etag` | TEXT | Yes | - | Entity tag for versioning |
510
+ | `metadata` | JSONB | Yes | - | Additional file metadata |
511
+ | `storage_type` | TEXT | No | - | `'disk'`, `'minio'`, or `'bun-s3'` |
512
+ | `is_synced` | BOOLEAN | No | `false` | Set `true` on every upload and every meta-links sync |
513
+ | `variant` | TEXT | Yes | - | Upload variant tag (e.g. `'thumbnail'`, `'original'`) |
514
+ | `principal_type` | TEXT | Yes | - | Associated principal type |
515
+ | `principal_id` | TEXT | Yes | - | Associated principal ID, always stored as a string |
244
516
 
245
517
  **Indexes:** `bucket_name`, `object_name`, `storage_type`, `is_synced`.
246
518
 
247
- > [!NOTE]
248
- > The `isSynced` field is automatically set to `true` when files are uploaded or synced via the meta-links endpoint. When a file is deleted, the MetaLink record is removed entirely. The `principalType`, `principalId`, and `variant` fields are only populated during upload when the corresponding query parameters are provided.
249
-
250
- ### MetaLink Tracking
519
+ `@model({ type: 'entity', skipMigrate: true })` on `BaseMetaLinkModel` means IGNIS's schema migration skips this table - it must be created manually, once, per database.
251
520
 
252
- When `useMetaLink: true`, the component:
521
+ ### MetaLink lifecycle
253
522
 
254
- - **On upload:** Creates a MetaLink database record for each uploaded file after fetching file stats from storage. If a `createMetaLink` callback is provided on `TMetaLinkConfig`, it is used instead of the default creation logic. Stores `principalType`, `principalId`, and `variant` from query parameters (if provided). The `principalId` is always coerced to a string via `String()`. If MetaLink creation fails, the upload still succeeds and the response includes `metaLink: null` with a `metaLinkError` message.
255
- - **On delete:** Initiates MetaLink record deletion as fire-and-forget (the `.then()/.catch()` promise chain is not awaited). The HTTP response with `{ "success": true }` returns before the database deletion completes. Deletion errors are logged but do not fail the request.
256
- - **On sync (PUT meta-links):** Checks if a MetaLink exists for the object (matched by `bucketName` + `objectName`). Updates it if found, creates a new one if not. Always sets `isSynced: true`. Returns `{ success: true, metaLink: ... }`.
523
+ - **On upload:** creates one MetaLink row per uploaded file after fetching fresh stats via `helper.getStat()`. Uses `metaLink.createMetaLink()` when provided, otherwise a default insert covering every standard field. `principalType`, `principalId`, and `variant` are taken from the upload's query parameters. If the insert throws, the upload still succeeds and the file's entry in the response gets `metaLink: null` plus a `metaLinkError` string; the error is also logged.
524
+ - **On delete:** storage delete happens first and is awaited; the MetaLink row delete (`deleteAll({ where: { bucketName, objectName } })`) is fired without awaiting it. The HTTP response returns as soon as the storage delete resolves - the database delete may still be in flight. Errors there are logged, never surfaced to the client.
525
+ - **On sync (`PUT meta-links/:objectName`):** looks up an existing row by `bucketName` + `objectName`. If found, `updateById()` then re-fetches with `findById()`. If not found, `create()`. Either path always sets `isSynced: true` and returns `{ success: true, metaLink }`.
257
526
 
258
- ## Component Lifecycle
527
+ ## Component lifecycle
259
528
 
260
- 1. **`binding()`** -- Reads `STATIC_ASSET_COMPONENT_OPTIONS` from the DI container
261
- 2. **Iterates each storage key** -- For each entry, calls `AssetControllerFactory.defineAssetController()`
262
- 3. **Generates default `normalizeLinkFn`** -- If not provided, creates links in the format <code v-pre>{basePath}/buckets/{bucket}/objects/{encodedName}</code>
263
- 4. **Registers controller** -- Calls `this.application.controller()` with the dynamically created class
264
- 5. **Logs binding** -- Logs the storage key, type, and MetaLink status for each registered backend
529
+ 1. `binding()` reads `STATIC_ASSET_COMPONENT_OPTIONS` from the DI container.
530
+ 2. Iterates each key in the options object.
531
+ 3. For each entry, builds a `normalizeLinkFn` default if the caller did not supply one (see [Default normalizeLinkFn](#default-normalizelinkfn)).
532
+ 4. Calls `AssetControllerFactory.defineAssetController()` and registers the result with `this.application.controller()`.
533
+ 5. Logs the storage key, storage type, and whether MetaLink is enabled for each registered backend.
265
534
 
266
- > [!TIP]
267
- > When MetaLink deletion fails on object delete, the error is logged but the HTTP response still returns `{ "success": true }`. Check your application logs if MetaLink records are not being cleaned up. Since the deletion is fire-and-forget, the response may return before the deletion attempt even starts.
535
+ `StaticAssetComponent` itself performs no eager configuration validation beyond the options type - a missing `metaLink` when `useMetaLink: true` is caught at compile time by the discriminated union, not at `binding()` runtime.
268
536
 
269
- ## See Also
537
+ ## See also
270
538
 
271
- - [Setup & Configuration](./) - Quick Reference, Setup Steps, Configuration Options
272
- - [Usage & Examples](./usage) - API Endpoints and Frontend Integration
273
- - [Error Reference](./errors) - Name Validation and Troubleshooting
539
+ - [Overview](./) - quick start, imports, and common configuration tasks
540
+ - [Usage & Examples](./usage) - task-oriented walkthroughs for every endpoint and MetaLink setup
541
+ - [Error Reference](./errors) - name validation rules and troubleshooting