@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,368 +1,248 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
The component dynamically generates REST endpoints for each configured storage backend. All backends expose the same API structure under their configured `basePath`.
|
|
8
|
-
|
|
9
|
-
| Method | Path | Description |
|
|
10
|
-
|--------|------|-------------|
|
|
11
|
-
| `GET` | <code v-pre>/{basePath}/buckets</code> | List all buckets |
|
|
12
|
-
| `GET` | <code v-pre>/{basePath}/buckets/:bucketName</code> | Get bucket by name |
|
|
13
|
-
| `POST` | <code v-pre>/{basePath}/buckets/:bucketName</code> | Create a bucket |
|
|
14
|
-
| `DELETE` | <code v-pre>/{basePath}/buckets/:bucketName</code> | Delete a bucket |
|
|
15
|
-
| `POST` | <code v-pre>/{basePath}/buckets/:bucketName/upload</code> | Upload files |
|
|
16
|
-
| `GET` | <code v-pre>/{basePath}/buckets/:bucketName/objects</code> | List objects |
|
|
17
|
-
| `GET` | <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName</code> | Stream file |
|
|
18
|
-
| `GET` | <code v-pre>/{basePath}/buckets/:bucketName/download/:objectName</code> | Download file |
|
|
19
|
-
| `DELETE` | <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName</code> | Delete object |
|
|
20
|
-
| `PUT` | <code v-pre>/{basePath}/buckets/:bucketName/meta-links/:objectName</code> | Sync MetaLink (MetaLink only) |
|
|
21
|
-
|
|
22
|
-
#### GET <code v-pre>/{basePath}/buckets</code>
|
|
23
|
-
**Response `200`:**
|
|
24
|
-
```json
|
|
25
|
-
[
|
|
26
|
-
{ "name": "my-bucket", "creationDate": "2025-01-01T00:00:00.000Z" }
|
|
27
|
-
]
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
#### GET <code v-pre>/{basePath}/buckets/:bucketName</code>
|
|
31
|
-
**Parameters:**
|
|
32
|
-
- `bucketName` (path): Bucket name
|
|
1
|
+
---
|
|
2
|
+
title: Static Asset Component - Usage & Examples
|
|
3
|
+
description: Task-oriented walkthroughs for buckets, uploads, downloads, MetaLink tracking, and frontend integration
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
33
6
|
|
|
34
|
-
|
|
7
|
+
# Usage & Examples
|
|
35
8
|
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{ "name": "my-bucket", "creationDate": "2025-01-01T00:00:00.000Z" }
|
|
39
|
-
```
|
|
9
|
+
Task-oriented patterns for the endpoints `StaticAssetComponent` generates, plus the full MetaLink tracking setup. All examples assume a backend registered under `basePath: '/assets'` - see [Overview](./) for the binding.
|
|
40
10
|
|
|
41
|
-
|
|
11
|
+
## List and manage buckets
|
|
42
12
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
13
|
+
```
|
|
14
|
+
GET /assets/buckets List all buckets
|
|
15
|
+
GET /assets/buckets/:bucketName Get one bucket (nullable)
|
|
16
|
+
POST /assets/buckets/:bucketName Create a bucket
|
|
17
|
+
DELETE /assets/buckets/:bucketName Delete a bucket
|
|
18
|
+
```
|
|
46
19
|
|
|
47
|
-
|
|
20
|
+
```typescript
|
|
21
|
+
const buckets = await fetch('/assets/buckets').then(r => r.json());
|
|
22
|
+
// [{ name: 'user-uploads', creationDate: '2026-01-01T00:00:00.000Z' }]
|
|
48
23
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{ "name": "my-bucket", "creationDate": "2025-12-13T00:00:00.000Z" }
|
|
24
|
+
await fetch('/assets/buckets/user-uploads', { method: 'POST' });
|
|
25
|
+
const { isDeleted } = await fetch('/assets/buckets/user-uploads', { method: 'DELETE' }).then(r => r.json());
|
|
52
26
|
```
|
|
53
27
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
#### DELETE <code v-pre>/{basePath}/buckets/:bucketName</code>
|
|
57
|
-
**Parameters:**
|
|
58
|
-
- `bucketName` (path): Bucket to delete
|
|
28
|
+
Every `bucketName` is validated with `isValidName()` - single segment, no `..`/`/`/`\`, no shell metacharacters, 255 characters or fewer. See [Error Reference](./errors) for the full rule set.
|
|
59
29
|
|
|
60
|
-
|
|
30
|
+
## Upload files
|
|
61
31
|
|
|
62
|
-
|
|
63
|
-
```json
|
|
64
|
-
{ "isDeleted": true }
|
|
65
|
-
```
|
|
32
|
+
`POST /assets/buckets/:bucketName/upload` accepts `multipart/form-data`, plus optional `principalType`, `principalId`, `variant`, and `folderPath` query parameters.
|
|
66
33
|
|
|
67
|
-
|
|
34
|
+
```typescript
|
|
35
|
+
const formData = new FormData();
|
|
36
|
+
formData.append('file', fileBlob, 'document.pdf');
|
|
68
37
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
38
|
+
const response = await fetch(
|
|
39
|
+
'/assets/buckets/user-uploads/upload?principalType=user&principalId=42&variant=original&folderPath=invoices/2026',
|
|
40
|
+
{ method: 'POST', body: formData },
|
|
41
|
+
);
|
|
72
42
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
- `variant` (optional, string): Variant tag for the upload (e.g., `"thumbnail"`, `"original"`)
|
|
77
|
-
- `folderPath` (optional, string): Target folder path for uploaded files (e.g., `"photos/2024"`). Each segment is validated with `isValidName()` and the segment count must not exceed the configured `maxFolderDepth` (default 2); returns 400 on violation
|
|
43
|
+
const [result] = await response.json();
|
|
44
|
+
// { bucketName: 'user-uploads', objectName: 'invoices/2026/document.pdf', link: '/assets/buckets/user-uploads/objects/invoices%2F2026%2Fdocument.pdf' }
|
|
45
|
+
```
|
|
78
46
|
|
|
79
|
-
|
|
47
|
+
- **`folderPath` is validated separately from the filename.** Each segment must pass `isValidName()`, and the segment count must not exceed `maxFolderDepth` (default `2`) - both return `400` before the file is even parsed.
|
|
48
|
+
- **`principalId` is always stored as a string**, coerced with `String()` regardless of whether you send a number or a string.
|
|
49
|
+
- With MetaLink enabled, the response also carries `metaLink` (the created database record) or, if that write failed, `metaLink: null` plus a `metaLinkError` string - **the upload itself still succeeds either way**.
|
|
80
50
|
|
|
81
|
-
|
|
51
|
+
## Stream or download an object
|
|
82
52
|
|
|
83
53
|
```typescript
|
|
84
|
-
const
|
|
85
|
-
files: z.union([z.instanceof(File), z.array(z.instanceof(File))]),
|
|
86
|
-
});
|
|
87
|
-
```
|
|
54
|
+
const objectName = 'invoices/2026/document.pdf';
|
|
88
55
|
|
|
89
|
-
|
|
56
|
+
// Inline stream - Content-Type comes from storage metadata, falls back to application/octet-stream
|
|
57
|
+
const streamUrl = `/assets/buckets/user-uploads/objects/${encodeURIComponent(objectName)}`;
|
|
90
58
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
"bucketName": "my-bucket",
|
|
96
|
-
"objectName": "file.pdf",
|
|
97
|
-
"link": "/assets/buckets/my-bucket/objects/file.pdf"
|
|
98
|
-
}
|
|
99
|
-
]
|
|
59
|
+
// Forces a browser download dialog via Content-Disposition: attachment
|
|
60
|
+
const downloadUrl = `/assets/buckets/user-uploads/download/${encodeURIComponent(objectName)}`;
|
|
61
|
+
window.open(downloadUrl, '_blank');
|
|
100
62
|
```
|
|
101
63
|
|
|
102
|
-
|
|
103
|
-
```json
|
|
104
|
-
[
|
|
105
|
-
{
|
|
106
|
-
"bucketName": "my-bucket",
|
|
107
|
-
"objectName": "file.pdf",
|
|
108
|
-
"link": "/assets/buckets/my-bucket/objects/file.pdf",
|
|
109
|
-
"metaLink": {
|
|
110
|
-
"id": "uuid",
|
|
111
|
-
"bucketName": "my-bucket",
|
|
112
|
-
"objectName": "file.pdf",
|
|
113
|
-
"link": "/assets/buckets/my-bucket/objects/file.pdf",
|
|
114
|
-
"mimetype": "application/pdf",
|
|
115
|
-
"size": 1024,
|
|
116
|
-
"etag": "abc123",
|
|
117
|
-
"metadata": {},
|
|
118
|
-
"storageType": "minio",
|
|
119
|
-
"isSynced": true,
|
|
120
|
-
"variant": "original",
|
|
121
|
-
"principalType": "user",
|
|
122
|
-
"principalId": "42",
|
|
123
|
-
"createdAt": "2025-12-15T03:00:00.000Z",
|
|
124
|
-
"modifiedAt": "2025-12-15T03:00:00.000Z"
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
]
|
|
128
|
-
```
|
|
64
|
+
Both routes validate `bucketName` with `isValidName()` and `objectName` with `isValidPath()`, then forward a fixed whitelist of metadata headers (`content-type`, `content-encoding`, `cache-control`, `etag`, `last-modified`) plus `X-Content-Type-Options: nosniff`. See [Header Sanitization](./api#header-sanitization) for the full list and why it exists.
|
|
129
65
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
[
|
|
133
|
-
{
|
|
134
|
-
"bucketName": "my-bucket",
|
|
135
|
-
"objectName": "file.pdf",
|
|
136
|
-
"link": "/assets/buckets/my-bucket/objects/file.pdf",
|
|
137
|
-
"metaLink": null,
|
|
138
|
-
"metaLinkError": "Database connection failed"
|
|
139
|
-
}
|
|
140
|
-
]
|
|
141
|
-
```
|
|
66
|
+
> [!TIP]
|
|
67
|
+
> `objectName` may embed folder segments (`invoices/2026/document.pdf`) - always pass the whole thing through `encodeURIComponent()`. Hono decodes it exactly once before the handler reads it, so a second `decodeURIComponent()` on your end is wrong and can corrupt names containing a literal `%`.
|
|
142
68
|
|
|
143
|
-
|
|
69
|
+
## List objects in a bucket
|
|
144
70
|
|
|
145
|
-
**Example:**
|
|
146
71
|
```typescript
|
|
147
|
-
const
|
|
148
|
-
|
|
72
|
+
const url = new URL('/assets/buckets/user-uploads/objects', location.origin);
|
|
73
|
+
url.searchParams.set('prefix', 'invoices/2026/');
|
|
74
|
+
url.searchParams.set('recursive', 'true'); // only the literal string "true" enables recursion
|
|
75
|
+
url.searchParams.set('maxKeys', '50');
|
|
149
76
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
'/assets/buckets/uploads/upload?principalType=user&principalId=123&variant=original',
|
|
153
|
-
{ method: 'POST', body: formData },
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const result = await response.json();
|
|
157
|
-
console.log(result[0].metaLink); // Database record (if MetaLink enabled)
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
#### GET <code v-pre>/{basePath}/buckets/:bucketName/objects</code>
|
|
161
|
-
**Parameters:**
|
|
162
|
-
- `bucketName` (path): Bucket name
|
|
163
|
-
|
|
164
|
-
**Validation:** Bucket name validated with `isValidName()`. Returns 400 `"Invalid bucket name"` if invalid.
|
|
165
|
-
|
|
166
|
-
**Query Parameters:**
|
|
167
|
-
- `prefix` (optional, string): Filter objects by prefix (e.g., `"folder/"`)
|
|
168
|
-
- `recursive` (optional, string): Recursive listing. Parsed via strict string comparison `=== 'true'` -- only the exact string `"true"` enables recursion; any other truthy value (e.g., `"1"`, `"yes"`) does not
|
|
169
|
-
- `maxKeys` (optional, string): Maximum number of objects to return. Parsed as integer via `parseInt(value, 10)`
|
|
170
|
-
|
|
171
|
-
**Response `200`:**
|
|
172
|
-
```json
|
|
173
|
-
[
|
|
174
|
-
{
|
|
175
|
-
"name": "file1.pdf",
|
|
176
|
-
"size": 1024,
|
|
177
|
-
"lastModified": "2025-12-13T00:00:00.000Z",
|
|
178
|
-
"etag": "abc123",
|
|
179
|
-
"prefix": "folder/"
|
|
180
|
-
}
|
|
181
|
-
]
|
|
77
|
+
const objects = await fetch(url).then(r => r.json());
|
|
78
|
+
// [{ name: 'invoices/2026/document.pdf', size: 1024, lastModified: '...', etag: '...' }]
|
|
182
79
|
```
|
|
183
80
|
|
|
184
|
-
|
|
81
|
+
`maxKeys`, if provided, must parse to a positive integer (`Number(maxKeys)` checked with `Number.isInteger`) or the endpoint returns `400`.
|
|
185
82
|
|
|
186
|
-
|
|
187
|
-
**Parameters:**
|
|
188
|
-
- `bucketName` (path): Bucket name
|
|
189
|
-
- `objectName` (path): Object name (URL-encoded)
|
|
83
|
+
## Delete an object
|
|
190
84
|
|
|
191
|
-
|
|
85
|
+
```typescript
|
|
86
|
+
const objectName = 'invoices/2026/document.pdf';
|
|
192
87
|
|
|
193
|
-
|
|
194
|
-
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
- Additional whitelisted headers forwarded from storage metadata (see [Header Sanitization](./api#header-sanitization))
|
|
88
|
+
const { success } = await fetch(
|
|
89
|
+
`/assets/buckets/user-uploads/objects/${encodeURIComponent(objectName)}`,
|
|
90
|
+
{ method: 'DELETE' },
|
|
91
|
+
).then(r => r.json());
|
|
92
|
+
```
|
|
199
93
|
|
|
200
|
-
|
|
201
|
-
**
|
|
202
|
-
- `bucketName` (path): Bucket name
|
|
203
|
-
- `objectName` (path): Object name (URL-encoded)
|
|
94
|
+
> [!NOTE]
|
|
95
|
+
> When MetaLink is enabled, the database record deletion is **fire-and-forget** - the response returns as soon as the storage delete completes, without waiting on the `deleteAll()` call. Deletion errors are logged but never fail the request.
|
|
204
96
|
|
|
205
|
-
|
|
97
|
+
## Sync a MetaLink record manually
|
|
206
98
|
|
|
207
|
-
|
|
208
|
-
- Streams file with download headers
|
|
209
|
-
- `Content-Disposition`: `attachment; filename="..."` (generated via `createContentDispositionHeader()`)
|
|
210
|
-
- `Content-Type`: From storage metadata or `application/octet-stream` as fallback
|
|
211
|
-
- `Content-Length`: File size in bytes
|
|
212
|
-
- `X-Content-Type-Options`: `nosniff`
|
|
213
|
-
- Additional whitelisted headers forwarded from storage metadata (see [Header Sanitization](./api#header-sanitization))
|
|
214
|
-
- Triggers browser download dialog
|
|
99
|
+
`PUT /assets/buckets/:bucketName/meta-links/:objectName` is only registered when `useMetaLink: true`. It re-reads the file's current storage metadata via `helper.getStat()` and creates or updates the matching MetaLink row.
|
|
215
100
|
|
|
216
|
-
**Example:**
|
|
217
101
|
```typescript
|
|
218
|
-
const
|
|
219
|
-
|
|
102
|
+
const objectName = 'invoices/2026/document.pdf';
|
|
103
|
+
|
|
104
|
+
const response = await fetch(
|
|
105
|
+
`/assets/buckets/user-uploads/meta-links/${encodeURIComponent(objectName)}`,
|
|
106
|
+
{ method: 'PUT' },
|
|
107
|
+
);
|
|
108
|
+
const { success, metaLink } = await response.json();
|
|
220
109
|
```
|
|
221
110
|
|
|
222
|
-
|
|
223
|
-
**Parameters:**
|
|
224
|
-
- `bucketName` (path): Bucket name
|
|
225
|
-
- `objectName` (path): Object to delete (URL-encoded)
|
|
111
|
+
Useful for backfilling MetaLink rows for files that already exist in storage, or after a database restore.
|
|
226
112
|
|
|
227
|
-
|
|
113
|
+
## Enable MetaLink tracking
|
|
228
114
|
|
|
229
|
-
|
|
230
|
-
- Deletes file from storage
|
|
231
|
-
- If MetaLink enabled, the MetaLink database record deletion is **fire-and-forget** -- the HTTP response returns immediately after the storage delete completes, without awaiting the database deletion
|
|
232
|
-
- MetaLink deletion errors are logged but do not fail the request
|
|
233
|
-
- MetaLink deletion uses `deleteAll({ where: { bucketName, objectName } })` to remove all matching records
|
|
115
|
+
MetaLink persists an upload's bucket, object name, link, mimetype, size, etag, storage type, principal, and variant to Postgres. `BaseMetaLinkModel` and `BaseMetaLinkRepository` cover the schema - you only write a repository subclass and the table.
|
|
234
116
|
|
|
235
|
-
**
|
|
236
|
-
```json
|
|
237
|
-
{ "success": true }
|
|
238
|
-
```
|
|
117
|
+
**1. Repository.** `BaseMetaLinkModel` is used as-is - no model subclass needed:
|
|
239
118
|
|
|
240
|
-
**Example:**
|
|
241
119
|
```typescript
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
120
|
+
import { repository } from '@venizia/ignis';
|
|
121
|
+
import { BaseMetaLinkModel, BaseMetaLinkRepository } from '@venizia/ignis/static-asset';
|
|
122
|
+
import { PostgresDataSource } from '@/datasources';
|
|
123
|
+
|
|
124
|
+
@repository({ model: BaseMetaLinkModel, dataSource: PostgresDataSource })
|
|
125
|
+
export class MetaLinkRepository extends BaseMetaLinkRepository {}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**2. Table.** `BaseMetaLinkModel` sets `skipMigrate: true`, so create it manually:
|
|
129
|
+
|
|
130
|
+
```sql
|
|
131
|
+
CREATE TABLE "MetaLink" (
|
|
132
|
+
id TEXT PRIMARY KEY,
|
|
133
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
134
|
+
modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
135
|
+
bucket_name TEXT NOT NULL,
|
|
136
|
+
object_name TEXT NOT NULL,
|
|
137
|
+
link TEXT NOT NULL,
|
|
138
|
+
mimetype TEXT NOT NULL,
|
|
139
|
+
size INTEGER NOT NULL,
|
|
140
|
+
etag TEXT,
|
|
141
|
+
metadata JSONB,
|
|
142
|
+
storage_type TEXT NOT NULL,
|
|
143
|
+
is_synced BOOLEAN NOT NULL DEFAULT false,
|
|
144
|
+
variant TEXT,
|
|
145
|
+
principal_type TEXT,
|
|
146
|
+
principal_id TEXT
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE INDEX "IDX_MetaLink_bucketName" ON "MetaLink"(bucket_name);
|
|
150
|
+
CREATE INDEX "IDX_MetaLink_objectName" ON "MetaLink"(object_name);
|
|
151
|
+
CREATE INDEX "IDX_MetaLink_storageType" ON "MetaLink"(storage_type);
|
|
152
|
+
CREATE INDEX "IDX_MetaLink_isSynced" ON "MetaLink"(is_synced);
|
|
250
153
|
```
|
|
251
154
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
```json
|
|
276
|
-
{
|
|
277
|
-
"success": true,
|
|
278
|
-
"metaLink": {
|
|
279
|
-
"id": "uuid",
|
|
280
|
-
"bucketName": "user-uploads",
|
|
281
|
-
"objectName": "document.pdf",
|
|
282
|
-
"link": "/assets/buckets/user-uploads/objects/document.pdf",
|
|
283
|
-
"mimetype": "application/pdf",
|
|
284
|
-
"size": 1048576,
|
|
285
|
-
"etag": "abc123",
|
|
286
|
-
"metadata": {},
|
|
287
|
-
"storageType": "minio",
|
|
288
|
-
"isSynced": true,
|
|
289
|
-
"principalType": null,
|
|
290
|
-
"principalId": null,
|
|
291
|
-
"createdAt": "2025-12-15T03:00:00.000Z",
|
|
292
|
-
"modifiedAt": "2025-12-15T03:00:00.000Z"
|
|
155
|
+
**3. Register the repository and wire it into the component options:**
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
export class Application extends BaseApplication {
|
|
159
|
+
preConfigure() {
|
|
160
|
+
this.repository(MetaLinkRepository);
|
|
161
|
+
|
|
162
|
+
this.bind<TStaticAssetsComponentOptions>({
|
|
163
|
+
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
164
|
+
}).toValue({
|
|
165
|
+
uploads: {
|
|
166
|
+
controller: { name: 'UploadsController', basePath: '/uploads' },
|
|
167
|
+
storage: StaticAssetStorageTypes.MINIO,
|
|
168
|
+
helper: new MinioHelper({ /* ... */ }),
|
|
169
|
+
useMetaLink: true,
|
|
170
|
+
metaLink: {
|
|
171
|
+
model: BaseMetaLinkModel,
|
|
172
|
+
repository: this.get<MetaLinkRepository>({ key: 'repositories.MetaLinkRepository' }),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
this.component(StaticAssetComponent);
|
|
293
178
|
}
|
|
294
179
|
}
|
|
295
180
|
```
|
|
296
181
|
|
|
297
|
-
|
|
182
|
+
> [!TIP]
|
|
183
|
+
> Call `this.repository(MetaLinkRepository)` before `this.get({ key: 'repositories.MetaLinkRepository' })` - the binding has to exist in the container first.
|
|
298
184
|
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
// Sync a single file
|
|
302
|
-
const bucketName = 'user-uploads';
|
|
303
|
-
const objectName = 'document.pdf';
|
|
185
|
+
### Custom MetaLink creation
|
|
304
186
|
|
|
305
|
-
|
|
306
|
-
`/assets/buckets/${bucketName}/meta-links/${encodeURIComponent(objectName)}`,
|
|
307
|
-
{ method: 'PUT' }
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
const result = await response.json();
|
|
311
|
-
console.log('Success:', result.success); // true
|
|
312
|
-
console.log('Synced:', result.metaLink.isSynced); // true
|
|
187
|
+
Provide `createMetaLink` on `TMetaLinkConfig` to fully replace the default insert - e.g. to add extra fields or run validation before persisting.
|
|
313
188
|
|
|
314
|
-
|
|
315
|
-
|
|
189
|
+
```typescript
|
|
190
|
+
metaLink: {
|
|
191
|
+
model: BaseMetaLinkModel,
|
|
192
|
+
repository: metaLinkRepository,
|
|
193
|
+
createMetaLink: async ({ uploadResult, fileStat, query }) =>
|
|
194
|
+
metaLinkRepository.create({
|
|
195
|
+
data: {
|
|
196
|
+
bucketName: uploadResult.bucketName,
|
|
197
|
+
objectName: uploadResult.objectName,
|
|
198
|
+
link: uploadResult.link,
|
|
199
|
+
mimetype: fileStat.metadata?.['mimetype'],
|
|
200
|
+
size: fileStat.size,
|
|
201
|
+
etag: fileStat.etag,
|
|
202
|
+
storageType: 'minio',
|
|
203
|
+
isSynced: true,
|
|
204
|
+
principalId: query.principalId ? String(query.principalId) : undefined,
|
|
205
|
+
principalType: query.principalType,
|
|
206
|
+
variant: query.variant,
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
},
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
When `createMetaLink` is omitted, the component uses a default insert that covers every standard field.
|
|
213
|
+
|
|
214
|
+
### Query MetaLink records
|
|
316
215
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
);
|
|
322
|
-
}
|
|
216
|
+
```typescript
|
|
217
|
+
const userFiles = await metaLinkRepository.find({ filter: { where: { principalType: 'user', principalId: '42' } } });
|
|
218
|
+
const thumbnails = await metaLinkRepository.find({ filter: { where: { variant: 'thumbnail' } } });
|
|
219
|
+
const pdfs = await metaLinkRepository.find({ filter: { where: { mimetype: 'application/pdf' } } });
|
|
323
220
|
```
|
|
324
221
|
|
|
325
|
-
## Frontend
|
|
222
|
+
## Frontend integration
|
|
326
223
|
|
|
327
224
|
```typescript
|
|
328
|
-
|
|
329
|
-
|
|
225
|
+
async function uploadFile(
|
|
226
|
+
file: File,
|
|
227
|
+
opts: { principalType?: string; principalId?: string; variant?: string } = {},
|
|
228
|
+
) {
|
|
330
229
|
const formData = new FormData();
|
|
331
230
|
formData.append('file', file);
|
|
332
231
|
|
|
333
|
-
const url = new URL('/assets/buckets/user-uploads/upload',
|
|
334
|
-
|
|
335
|
-
if (principalId) url.searchParams.append('principalId', principalId);
|
|
336
|
-
if (variant) url.searchParams.append('variant', variant);
|
|
337
|
-
|
|
338
|
-
const response = await fetch(url, {
|
|
339
|
-
method: 'POST',
|
|
340
|
-
body: formData,
|
|
341
|
-
});
|
|
232
|
+
const url = new URL('/assets/buckets/user-uploads/upload', location.origin);
|
|
233
|
+
Object.entries(opts).forEach(([key, value]) => value && url.searchParams.set(key, value));
|
|
342
234
|
|
|
343
|
-
const result = await
|
|
344
|
-
return result
|
|
235
|
+
const [result] = await fetch(url, { method: 'POST', body: formData }).then(r => r.json());
|
|
236
|
+
return result.link;
|
|
345
237
|
}
|
|
346
238
|
|
|
347
|
-
// Download file
|
|
348
239
|
function downloadFile(bucketName: string, objectName: string) {
|
|
349
|
-
|
|
350
|
-
window.open(url, '_blank');
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// List files in bucket
|
|
354
|
-
async function listFiles(bucketName: string, prefix?: string, recursive?: boolean) {
|
|
355
|
-
const url = new URL(`/assets/buckets/${bucketName}/objects`, window.location.origin);
|
|
356
|
-
if (prefix) url.searchParams.append('prefix', prefix);
|
|
357
|
-
if (recursive) url.searchParams.append('recursive', 'true');
|
|
358
|
-
|
|
359
|
-
const response = await fetch(url);
|
|
360
|
-
return await response.json();
|
|
240
|
+
window.open(`/assets/buckets/${bucketName}/download/${encodeURIComponent(objectName)}`, '_blank');
|
|
361
241
|
}
|
|
362
242
|
```
|
|
363
243
|
|
|
364
|
-
## See
|
|
244
|
+
## See also
|
|
365
245
|
|
|
366
|
-
- [
|
|
367
|
-
- [
|
|
368
|
-
- [Error Reference](./errors) -
|
|
246
|
+
- [Overview](./) - quick start, imports, and common configuration tasks
|
|
247
|
+
- [Full Reference](./api) - request/response schemas, `IStorageHelper` interface, header sanitization, internals
|
|
248
|
+
- [Error Reference](./errors) - name validation rules and troubleshooting
|