@venizia/ignis-docs 0.2.0 → 0.2.1-1

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 (174) 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 +24 -13
  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 +27 -3
  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 +8 -4
  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 +247 -153
  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 +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -1,46 +1,180 @@
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-server/src/components/static-asset/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/component.ts)
14
+ - [`packages/core-server/src/components/static-asset/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/types.ts)
15
+ - [`packages/core-server/src/components/static-asset/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/constants.ts)
16
+ - [`packages/core-server/src/components/static-asset/common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/keys.ts)
17
+ - [`packages/core-server/src/components/static-asset/controller/factory.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/controller/factory.ts)
18
+ - [`packages/core-server/src/components/static-asset/controller/base.definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/controller/base.definition.ts)
19
+ - [`packages/core-server/src/components/static-asset/models/base.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/models/base.model.ts)
20
+ - [`packages/core-server/src/components/static-asset/repositories/base.repository.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/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, for example `'/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 argument 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 |
27
161
 
28
- The factory method accepts the following options:
162
+ ### Default `normalizeLinkFn`
163
+
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.
174
+
175
+ `BaseStorageHelper` has its own backend-specific `normalizeObjectLink()`. It only runs when you call a helper's `upload()` directly, outside the component. Through `StaticAssetComponent`, the component's default `normalizeLinkFn` always takes priority instead.
42
176
 
43
- A constants class following the IGNIS pattern with `static readonly` fields, a `SCHEME_SET`, and an `isValid()` method:
177
+ ## Storage types
44
178
 
45
179
  ```typescript
46
180
  class StaticAssetStorageTypes {
@@ -56,95 +190,38 @@ class StaticAssetStorageTypes {
56
190
  }
57
191
 
58
192
  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
- });
193
+ // 'disk' | 'minio' | 'bun-s3'
76
194
  ```
77
195
 
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
196
+ | Type | Constant | Helper | Requires |
197
+ |------|----------|--------|----------|
198
+ | `'disk'` | `StaticAssetStorageTypes.DISK` | `DiskHelper` | Local filesystem write access |
199
+ | `'minio'` | `StaticAssetStorageTypes.MINIO` | `MinioHelper` | A MinIO or S3-compatible endpoint |
200
+ | `'bun-s3'` | `StaticAssetStorageTypes.BUN_S3` | `BunS3Helper` | Bun runtime (imports Bun's native `S3Client`) |
81
201
 
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.
202
+ ## Storage helpers
83
203
 
84
- ### WHITELIST_HEADERS
204
+ ### `IStorageHelper` interface
85
205
 
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)
111
- ```
112
-
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
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
-
126
- ## IStorageHelper Interface
127
-
128
- All storage helpers implement this unified interface:
206
+ Every backend implements this contract; `BaseStorageHelper` (abstract) implements the shared parts (`isValidName`, `isValidPath`, `upload`, `getMimeType`, `getFileType`) and leaves the rest abstract.
129
207
 
130
208
  ```typescript
131
209
  interface IStorageHelper {
132
210
  isValidName(name: string): boolean;
133
211
  isValidPath(pathStr: string, opts?: { maxDepth?: number }): boolean;
134
212
 
135
- // Bucket operations
136
213
  isBucketExists(opts: { name: string }): Promise<boolean>;
137
214
  getBuckets(): Promise<IBucketInfo[]>;
138
215
  getBucket(opts: { name: string }): Promise<IBucketInfo | null>;
139
216
  createBucket(opts: { name: string }): Promise<IBucketInfo | null>;
140
217
  removeBucket(opts: { name: string }): Promise<boolean>;
141
218
 
142
- // File operations
143
219
  upload(opts: {
144
220
  bucket: string;
145
221
  files: IUploadFile[];
146
222
  normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
147
223
  normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
224
+ maxFolderDepth?: number;
148
225
  }): Promise<IUploadResult[]>;
149
226
 
150
227
  getFile(opts: { bucket: string; name: string; options?: any }): Promise<Readable>;
@@ -153,12 +230,21 @@ interface IStorageHelper {
153
230
  removeObjects(opts: { bucket: string; names: string[] }): Promise<void>;
154
231
  listObjects(opts: IListObjectsOptions): Promise<IObjectInfo[]>;
155
232
 
156
- // Utility
157
233
  getFileType(opts: { mimeType: string }): string;
158
234
  }
159
235
  ```
160
236
 
161
- ### Supporting Types
237
+ ```
238
+ IStorageHelper (interface)
239
+ |
240
+ BaseStorageHelper (abstract - implements isValidName/isValidPath/upload/getMimeType/getFileType)
241
+ |
242
+ +-- DiskHelper (local filesystem)
243
+ +-- MinioHelper (S3-compatible)
244
+ +-- BunS3Helper (Bun-native S3, Bun only)
245
+ ```
246
+
247
+ ### Supporting types
162
248
 
163
249
  ```typescript
164
250
  interface IUploadFile {
@@ -208,66 +294,300 @@ interface IListObjectsOptions {
208
294
  }
209
295
  ```
210
296
 
211
- ### Storage Helper Hierarchy
297
+ ### Name and path validation (`BaseStorageHelper`)
298
+
299
+ `isValidName(name)` rejects any of the following, in order:
300
+
301
+ | Check | Rejects |
302
+ |-------|---------|
303
+ | Type | Non-string input |
304
+ | Empty | Empty string |
305
+ | Path traversal | `..`, `/`, or `\` |
306
+ | Hidden file | A leading `.` |
307
+ | Shell metacharacters | `;`, `\|`, `&`, `$`, `` ` ``, `<`, `>`, `{`, `}`, `[`, `]`, `!`, `#` |
308
+ | Control characters | `\n`, `\r`, `\0` |
309
+ | Length | Over 255 characters |
310
+ | Whitespace-only | Empty after trimming |
311
+
312
+ `isValidPath(pathStr, { maxDepth })` runs these checks, in order:
212
313
 
314
+ | Step | Behavior |
315
+ |------|----------|
316
+ | Normalize | Trims leading/trailing slashes |
317
+ | Empty check | Rejects if the result is empty |
318
+ | Double slashes | Rejects empty segments, for example `a//b` |
319
+ | Folder depth | `folderDepth = segments.length - 1`; rejects if it exceeds `maxDepth` (default `BaseStorageHelper.DEFAULT_MAX_FOLDER_DEPTH = 2`) |
320
+ | Segment names | Every segment must pass `isValidName()` |
321
+ | Path length | Rejects a normalized length over 1024 characters |
322
+
323
+ ### `DiskHelper`
324
+
325
+ ```typescript
326
+ interface IDiskHelperOptions {
327
+ basePath: string; // Base directory for storage
328
+ scope?: string; // Logger scope, default: 'DiskHelper'
329
+ identifier?: string; // Helper identifier, default: 'DiskHelper'
330
+ }
213
331
  ```
214
- IStorageHelper (interface)
215
- |
216
- BaseStorageHelper (abstract class)
217
- |
218
- +-- DiskHelper (local filesystem)
219
- +-- MinioHelper (S3-compatible)
220
- +-- BunS3Helper (Bun-native S3)
332
+
333
+ 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.
334
+
335
+ ```typescript
336
+ const diskHelper = new DiskHelper({ basePath: './app_data/storage' });
337
+ ```
338
+
339
+ ### `MinioHelper`
340
+
341
+ ```typescript
342
+ interface IMinioHelperOptions extends ClientOptions { // minio's own SDK options, plus:
343
+ scope?: string;
344
+ identifier?: string;
345
+ }
346
+ ```
347
+
348
+ `ClientOptions` comes straight from the `minio` package - `endPoint`, `port`, `useSSL`, `accessKey`, `secretKey`, and the rest of the MinIO client's own configuration surface.
349
+
350
+ ```typescript
351
+ const minioHelper = new MinioHelper({
352
+ endPoint: 'minio.example.com',
353
+ port: 9000,
354
+ useSSL: true,
355
+ accessKey: process.env.MINIO_ACCESS_KEY,
356
+ secretKey: process.env.MINIO_SECRET_KEY,
357
+ });
358
+ ```
359
+
360
+ ### `BunS3Helper`
361
+
362
+ ```typescript
363
+ interface IBunS3HelperOptions {
364
+ accessKey: string;
365
+ secretKey: string;
366
+ endpoint: string;
367
+ region?: string; // Default: 'us-east-1'
368
+ sessionToken?: string;
369
+ scope?: string;
370
+ identifier?: string;
371
+ }
221
372
  ```
222
373
 
223
- ## MetaLink SQL Schema
374
+ Wraps Bun's native `S3Client`, imported from the `bun` builtin module. That module only resolves under the Bun runtime. This is why `BunS3Helper` is exported from the separate `@venizia/ignis-helpers/bun-s3` subpath, not the main entry point.
375
+
376
+ ```typescript
377
+ import { BunS3Helper } from '@venizia/ignis-helpers/bun-s3';
378
+
379
+ const bunS3Helper = new BunS3Helper({
380
+ accessKey: process.env.S3_ACCESS_KEY,
381
+ secretKey: process.env.S3_SECRET_KEY,
382
+ endpoint: 'https://s3.us-east-1.amazonaws.com',
383
+ });
384
+ ```
385
+
386
+ ## Controller factory
387
+
388
+ `AssetControllerFactory.defineAssetController(opts: IAssetControllerOptions)` builds one controller class per call:
389
+
390
+ ```typescript
391
+ interface IAssetControllerOptions {
392
+ controller: TStaticAssetsComponentOptions[string]['controller'];
393
+ storage: TStaticAssetStorageType;
394
+ helper: IStorageHelper;
395
+ useMetaLink?: boolean;
396
+ metaLink?: TMetaLinkConfig;
397
+ options?: TStaticAssetExtraOptions;
398
+ }
399
+ ```
400
+
401
+ 1. Creates a class extending `BaseRestController`, decorated `@controller({ path: basePath })`.
402
+ 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.
403
+ 3. Binds every route in `binding()` with `this.bindRoute({ configs }).to({ handler })`, spread-merging each base definition with its `routes?.<key>` override.
404
+ 4. Registers `recreateMetaLink` only when `useMetaLink && metaLink` are both set.
405
+ 5. `StaticAssetComponent.binding()` registers the resulting class with `this.application.controller(...)`.
406
+
407
+ ```
408
+ StaticAssetComponent.binding()
409
+ | iterates componentOptions
410
+ AssetControllerFactory.defineAssetController({ controller, storage, helper, ... })
411
+ | creates
412
+ @controller({ path: basePath })
413
+ class GeneratedStaticAssetController extends BaseRestController { ... }
414
+ | registered via
415
+ this.application.controller(GeneratedStaticAssetController)
416
+ ```
417
+
418
+ ### `MultipartBodySchema`
419
+
420
+ The Zod schema validating the `upload` request body:
421
+
422
+ ```typescript
423
+ const MultipartBodySchema = z.object({
424
+ files: z.union([z.instanceof(File), z.array(z.instanceof(File))]).openapi({
425
+ type: 'array',
426
+ items: { type: 'string', format: 'binary' },
427
+ }),
428
+ });
429
+ ```
430
+
431
+ ### Endpoint reference
432
+
433
+ | Method | Path | Notes |
434
+ |--------|------|-------|
435
+ | `GET` | `/buckets` | No params. Returns `IBucketInfo[]` |
436
+ | `GET` | <code v-pre>/buckets/{bucketName}</code> | Returns `IBucketInfo \| null` |
437
+ | `POST` | <code v-pre>/buckets/{bucketName}</code> | Returns the created `IBucketInfo`. Throws if the bucket already exists or the name is invalid - see [Error Reference](./errors) |
438
+ | `DELETE` | <code v-pre>/buckets/{bucketName}</code> | Returns <code v-pre>{ isDeleted: boolean }</code> |
439
+ | `POST` | <code v-pre>/buckets/{bucketName}/upload</code> | `multipart/form-data` body; query: `principalType?`, `principalId?`, `variant?`, `folderPath?`. Returns `IUploadResult[]` |
440
+ | `GET` | <code v-pre>/buckets/{bucketName}/objects</code> | Query: `prefix?`, `recursive?` (`'true'` string only), `maxKeys?` (positive integer string). Returns `IObjectInfo[]` |
441
+ | `GET` | <code v-pre>/buckets/{bucketName}/objects/{objectName}</code> | Streams the file inline. `objectName` is a single percent-encoded segment |
442
+ | `GET` | <code v-pre>/buckets/{bucketName}/download/{objectName}</code> | Streams the file with `Content-Disposition: attachment` |
443
+ | `DELETE` | <code v-pre>/buckets/{bucketName}/objects/{objectName}</code> | Returns <code v-pre>{ success: boolean }</code> |
444
+ | `PUT` | <code v-pre>/buckets/{bucketName}/meta-links/{objectName}</code> | Only registered when `useMetaLink: true`. Returns <code v-pre>{ success: boolean, metaLink }</code> |
445
+
446
+ ### Upload validation order
447
+
448
+ 1. `bucketName` validated with `isValidName()` - `400 "Invalid bucket name"` on failure.
449
+ 2. If `folderPath` is present, three checks run in order:
450
+
451
+ | Check | Failure |
452
+ |-------|---------|
453
+ | Trim leading/trailing slashes | `400 "Invalid folder path"` if empty after trimming |
454
+ | Segment count vs. `maxFolderDepth` | `400 "Folder path exceeds max depth of {n}"` if over |
455
+ | Each segment via `isValidName()` | `400 "Invalid folder path segment: {segment}"` if any fails |
456
+
457
+ 3. `multipart/form-data` parsed via `parseMultipartBody()`.
458
+ 4. Each file's effective buffer is checked non-empty - direct `buffer`, or `readFileSync(file.path)` when `storage: 'disk'` was used. Empty content returns `400 "Empty file content | name: {originalName}"`.
459
+ 5. `helper.upload()` runs the storage-helper-level checks below.
460
+ 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.
461
+
462
+ ### Storage-helper-level upload checks (`BaseStorageHelper.upload`)
463
+
464
+ These run inside `helper.upload()`, separate from the controller checks above. They are reachable even when a caller uses the storage helper directly:
465
+
466
+ | Check | Error message | Default status |
467
+ |-------|----------------|-----------------|
468
+ | Bucket does not exist (`isBucketExists()` false) | <code v-pre>[upload] Bucket does not exist \| name: {bucket}</code> | `400` |
469
+ | `originalName` fails `isValidName()` | `[upload] Invalid original file name` | `400` |
470
+ | `folderPath` segment count exceeds `maxFolderDepth` | <code v-pre>[upload] Invalid folder path \| depth: {n} \| max: {m}</code> | `400` |
471
+ | `folderPath` fails `isValidPath()` for any other reason | `[upload] Invalid folder path` | `400` |
472
+ | `size` is `undefined`, `null`, or negative | <code v-pre>[upload] Invalid file size \| size: {size}</code> | `400` |
473
+ | Normalized name (post `normalizeNameFn`) fails `isValidPath()` | <code v-pre>[upload] Invalid normalized object name \| name: {name}</code> | `400` |
474
+
475
+ `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`.
476
+
477
+ ## Header sanitization
478
+
479
+ ```typescript
480
+ const WHITELIST_HEADERS = [
481
+ 'content-type',
482
+ 'content-encoding',
483
+ 'cache-control',
484
+ 'etag',
485
+ 'last-modified',
486
+ ] as const;
487
+ ```
488
+
489
+ These correspond to `HTTP.Headers.CONTENT_TYPE`, `CONTENT_ENCODING`, `CACHE_CONTROL`, `ETAG`, and `LAST_MODIFIED` from `@venizia/ignis-helpers`.
490
+
491
+ 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.
492
+
493
+ All streaming responses also set:
494
+
495
+ ```http
496
+ X-Content-Type-Options: nosniff
497
+ Content-Type: <from metadata, or application/octet-stream as fallback>
498
+ Content-Length: <file size in bytes>
499
+ Content-Disposition: attachment; filename="..." (download endpoint only, via createContentDispositionHeader())
500
+ ```
501
+
502
+ ## Object name decoding
503
+
504
+ 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()`:
505
+
506
+ - `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 - the object would become permanently unfetchable and undeletable.
507
+ - An object named `a%2Fb.png` would decode twice into `a/b.png` - a different object than the one requested.
508
+
509
+ `isValidName()`/`isValidPath()` still run on the singly-decoded value, so a traversal payload is rejected exactly as before.
510
+
511
+ ## `TMetaLinkConfig`
512
+
513
+ ```typescript
514
+ type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
515
+ model: typeof BaseRelationalEntity<Schema>;
516
+ repository: DefaultRelationalRepository<Schema>;
517
+ createMetaLink?: (opts: {
518
+ uploadResult: IUploadResult;
519
+ fileStat: IFileStat;
520
+ query: TUploadQuery;
521
+ }) => ValueOrPromise<{ count: number; data: Schema }>;
522
+ };
523
+ ```
524
+
525
+ | Canonical name | Alias |
526
+ |----------------|-------|
527
+ | `BaseRelationalEntity` | `BasePostgresEntity` |
528
+ | `DefaultRelationalRepository` | `DefaultCRUDRepository` |
529
+
530
+ Both canonical classes live in `packages/core-server/src/connectors/postgres/`. Each alias re-exports the same class under a different name.
531
+
532
+ ## MetaLink SQL schema
224
533
 
225
534
  **Table:** `MetaLink`
226
535
 
227
536
  | Field | Type | Nullable | Default | Description |
228
537
  |-------|------|----------|---------|-------------|
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) |
538
+ | `id` | TEXT | No | - | Primary key |
539
+ | `created_at` | TIMESTAMPTZ | No | `NOW()` | Row creation time |
540
+ | `modified_at` | TIMESTAMPTZ | No | `NOW()` | Row last-update time |
541
+ | `bucket_name` | TEXT | No | - | Storage bucket name |
542
+ | `object_name` | TEXT | No | - | File object name (may include folder segments) |
543
+ | `link` | TEXT | No | - | Access URL to the file |
544
+ | `mimetype` | TEXT | No | - | File MIME type |
545
+ | `size` | INTEGER | No | - | File size in bytes |
546
+ | `etag` | TEXT | Yes | - | Entity tag for versioning |
547
+ | `metadata` | JSONB | Yes | - | Additional file metadata |
548
+ | `storage_type` | TEXT | No | - | `'disk'`, `'minio'`, or `'bun-s3'` |
549
+ | `is_synced` | BOOLEAN | No | `false` | Set `true` on every upload and every meta-links sync |
550
+ | `variant` | TEXT | Yes | - | Upload variant tag (for example `'thumbnail'`, `'original'`) |
551
+ | `principal_type` | TEXT | Yes | - | Associated principal type |
552
+ | `principal_id` | TEXT | Yes | - | Associated principal ID, always stored as a string |
244
553
 
245
554
  **Indexes:** `bucket_name`, `object_name`, `storage_type`, `is_synced`.
246
555
 
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.
556
+ `@model({ type: 'entity', skipMigrate: true })` on `BaseMetaLinkModel` means IGNIS's schema migration skips this table. Create it manually, once, per database.
557
+
558
+ ### MetaLink lifecycle
249
559
 
250
- ### MetaLink Tracking
560
+ - **On upload:**
561
+ - Creates one MetaLink row per uploaded file, after fetching fresh stats via `helper.getStat()`.
562
+ - Uses `metaLink.createMetaLink()` when provided, otherwise a default insert that covers every standard field.
563
+ - `principalType`, `principalId`, and `variant` come from the upload's query parameters.
564
+ - If the insert throws, the upload still succeeds. The file's response entry gets `metaLink: null` plus a `metaLinkError` string, and the error is logged.
565
+ - **On delete:**
566
+ - The storage delete happens first and is awaited.
567
+ - The MetaLink row delete (`deleteAll({ where: { bucketName, objectName } })`) fires without being awaited.
568
+ - The HTTP response returns as soon as the storage delete resolves - the database delete may still be in flight.
569
+ - Errors there are logged, never surfaced to the client.
570
+ - **On sync (`PUT meta-links/:objectName`):** looks up an existing row by `bucketName` + `objectName`.
251
571
 
252
- When `useMetaLink: true`, the component:
572
+ | Row found? | Action |
573
+ |------------|--------|
574
+ | Yes | `updateById()`, then re-fetches with `findById()` |
575
+ | No | `create()` |
253
576
 
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: ... }`.
577
+ Either path sets `isSynced: true` and returns `{ success: true, metaLink }`.
257
578
 
258
- ## Component Lifecycle
579
+ ## Component lifecycle
259
580
 
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
581
+ 1. `binding()` reads `STATIC_ASSET_COMPONENT_OPTIONS` from the DI container.
582
+ 2. Iterates each key in the options object.
583
+ 3. For each entry, builds a `normalizeLinkFn` default if the caller did not supply one (see [Default normalizeLinkFn](#default-normalizelinkfn)).
584
+ 4. Calls `AssetControllerFactory.defineAssetController()` and registers the result with `this.application.controller()`.
585
+ 5. Logs the storage key, storage type, and whether MetaLink is enabled for each registered backend.
265
586
 
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.
587
+ `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
588
 
269
- ## See Also
589
+ ## See also
270
590
 
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
591
+ - [Overview](./) - quick start, imports, and common configuration tasks
592
+ - [Usage & Examples](./usage) - task-oriented walkthroughs for every endpoint and MetaLink setup
593
+ - [Error Reference](./errors) - name validation rules and troubleshooting