@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.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- 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 +27 -3
- 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 +8 -4
- 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 +247 -153
- 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 +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- 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/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- 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 +58 -218
- package/content/references/utilities/retry.md +139 -0
- 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/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +24 -23
|
@@ -1,46 +1,180 @@
|
|
|
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-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
|
-
|
|
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, 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
202
|
+
## Storage helpers
|
|
83
203
|
|
|
84
|
-
###
|
|
204
|
+
### `IStorageHelper` interface
|
|
85
205
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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 |
|
|
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 |
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
572
|
+
| Row found? | Action |
|
|
573
|
+
|------------|--------|
|
|
574
|
+
| Yes | `updateById()`, then re-fetches with `findById()` |
|
|
575
|
+
| No | `create()` |
|
|
253
576
|
|
|
254
|
-
|
|
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
|
|
579
|
+
## Component lifecycle
|
|
259
580
|
|
|
260
|
-
1.
|
|
261
|
-
2.
|
|
262
|
-
3.
|
|
263
|
-
4.
|
|
264
|
-
5.
|
|
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
|
-
|
|
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
|
|
589
|
+
## See also
|
|
270
590
|
|
|
271
|
-
- [
|
|
272
|
-
- [Usage & Examples](./usage) -
|
|
273
|
-
- [Error Reference](./errors) -
|
|
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
|