@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.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/package.json +8 -8
|
@@ -1,46 +1,178 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
+
| Key | Constant | Type | Required | Default |
|
|
68
|
+
|-----|----------|------|----------|---------|
|
|
69
|
+
| `@app/static-asset-component/options` | `StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS` | `TStaticAssetsComponentOptions` | Yes | `{}` |
|
|
6
70
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
200
|
+
## Storage helpers
|
|
118
201
|
|
|
119
|
-
|
|
202
|
+
### `IStorageHelper` interface
|
|
120
203
|
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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 |
|
|
230
|
-
| `created_at` | TIMESTAMPTZ | No | `NOW()` |
|
|
231
|
-
| `modified_at` | TIMESTAMPTZ | No | `NOW()` |
|
|
232
|
-
| `bucket_name` | TEXT | No |
|
|
233
|
-
| `object_name` | TEXT | No |
|
|
234
|
-
| `link` | TEXT | No |
|
|
235
|
-
| `mimetype` | TEXT | No |
|
|
236
|
-
| `size` | INTEGER | No |
|
|
237
|
-
| `etag` | TEXT | Yes |
|
|
238
|
-
| `metadata` | JSONB | Yes |
|
|
239
|
-
| `storage_type` | TEXT | No |
|
|
240
|
-
| `is_synced` | BOOLEAN | No | `false` |
|
|
241
|
-
| `variant` | TEXT | Yes |
|
|
242
|
-
| `principal_type` | TEXT | Yes |
|
|
243
|
-
| `principal_id` | TEXT | Yes |
|
|
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
|
-
|
|
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
|
-
|
|
521
|
+
### MetaLink lifecycle
|
|
253
522
|
|
|
254
|
-
- **On upload:**
|
|
255
|
-
- **On delete:**
|
|
256
|
-
- **On sync (PUT meta-links):**
|
|
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
|
|
527
|
+
## Component lifecycle
|
|
259
528
|
|
|
260
|
-
1.
|
|
261
|
-
2.
|
|
262
|
-
3.
|
|
263
|
-
4.
|
|
264
|
-
5.
|
|
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
|
-
|
|
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
|
|
537
|
+
## See also
|
|
270
538
|
|
|
271
|
-
- [
|
|
272
|
-
- [Usage & Examples](./usage) -
|
|
273
|
-
- [Error Reference](./errors) -
|
|
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
|