@venizia/ignis-docs 0.0.8 → 0.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 +7 -7
- package/content/best-practices/api-usage-examples.md +15 -12
- package/content/best-practices/architectural-patterns.md +70 -78
- package/content/best-practices/architecture-decisions.md +91 -60
- package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
- package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
- package/content/best-practices/code-style-standards/control-flow.md +5 -2
- package/content/best-practices/code-style-standards/documentation.md +13 -13
- package/content/best-practices/code-style-standards/function-patterns.md +9 -10
- package/content/best-practices/code-style-standards/index.md +1 -1
- package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
- package/content/best-practices/code-style-standards/route-definitions.md +30 -12
- package/content/best-practices/code-style-standards/tooling.md +8 -5
- package/content/best-practices/code-style-standards/type-safety.md +13 -12
- package/content/best-practices/common-pitfalls.md +56 -37
- package/content/best-practices/contribution-workflow.md +13 -14
- package/content/best-practices/data-modeling.md +44 -20
- package/content/best-practices/deployment-strategies.md +28 -27
- package/content/best-practices/error-handling.md +48 -24
- package/content/best-practices/index.md +5 -5
- package/content/best-practices/performance-optimization.md +36 -28
- package/content/best-practices/security-guidelines.md +52 -23
- package/content/best-practices/testing-strategies.md +65 -51
- package/content/best-practices/troubleshooting-tips.md +24 -24
- package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
- package/content/extensions/components/authentication/api.md +19 -19
- package/content/extensions/components/authentication/errors.md +7 -7
- package/content/extensions/components/authentication/index.md +10 -8
- package/content/extensions/components/authentication/usage.md +101 -6
- package/content/extensions/components/authorization/api.md +45 -25
- package/content/extensions/components/authorization/errors.md +6 -6
- package/content/extensions/components/authorization/index.md +11 -10
- package/content/extensions/components/authorization/usage.md +21 -21
- package/content/extensions/components/health-check.md +1 -1
- package/content/extensions/components/index.md +5 -5
- package/content/extensions/components/mail/errors.md +15 -15
- package/content/extensions/components/mail/index.md +1 -2
- package/content/extensions/components/mail/usage.md +1 -1
- package/content/extensions/components/request-tracker.md +1 -1
- package/content/extensions/components/socket-io/api.md +9 -9
- package/content/extensions/components/socket-io/errors.md +5 -5
- package/content/extensions/components/socket-io/index.md +8 -8
- package/content/extensions/components/socket-io/usage.md +1 -1
- package/content/extensions/components/static-asset/api.md +17 -4
- package/content/extensions/components/static-asset/errors.md +4 -4
- package/content/extensions/components/static-asset/index.md +26 -28
- package/content/extensions/components/static-asset/usage.md +13 -12
- package/content/extensions/components/template/index.md +2 -2
- package/content/extensions/components/template/setup-page.md +1 -1
- package/content/extensions/components/websocket/api.md +3 -3
- package/content/extensions/components/websocket/errors.md +5 -5
- package/content/extensions/components/websocket/index.md +5 -5
- package/content/extensions/components/websocket/usage.md +3 -3
- package/content/extensions/helpers/cron/index.md +2 -2
- package/content/extensions/helpers/crypto/index.md +1 -1
- package/content/extensions/helpers/env/index.md +27 -12
- package/content/extensions/helpers/error/index.md +81 -25
- package/content/extensions/helpers/index.md +2 -3
- package/content/extensions/helpers/inversion/index.md +15 -7
- package/content/extensions/helpers/kafka/examples.md +1 -1
- package/content/extensions/helpers/logger/index.md +32 -2
- package/content/extensions/helpers/network/index.md +6 -0
- package/content/extensions/helpers/queue/index.md +14 -17
- package/content/extensions/helpers/redis/index.md +548 -323
- package/content/extensions/helpers/socket-io/index.md +14 -10
- package/content/extensions/helpers/storage/api.md +44 -8
- package/content/extensions/helpers/storage/index.md +43 -7
- package/content/extensions/helpers/template/index.md +6 -3
- package/content/extensions/helpers/types/index.md +11 -8
- package/content/extensions/helpers/websocket/api.md +9 -9
- package/content/extensions/helpers/websocket/index.md +7 -7
- package/content/extensions/helpers/worker-thread/index.md +2 -2
- package/content/extensions/index.md +3 -4
- package/content/extensions/src-details/mcp-server.md +18 -24
- package/content/guides/core-concepts/application/bootstrapping.md +11 -14
- package/content/guides/core-concepts/application/index.md +3 -3
- package/content/guides/core-concepts/components.md +19 -10
- package/content/guides/core-concepts/dependency-injection.md +6 -3
- package/content/guides/core-concepts/grpc-controllers.md +6 -5
- package/content/guides/core-concepts/persistent/datasources.md +33 -27
- package/content/guides/core-concepts/persistent/index.md +16 -5
- package/content/guides/core-concepts/persistent/models.md +24 -20
- package/content/guides/core-concepts/persistent/postgres-drivers.md +167 -0
- package/content/guides/core-concepts/persistent/repositories.md +40 -23
- package/content/guides/core-concepts/persistent/search-meilisearch.md +183 -0
- package/content/guides/core-concepts/persistent/search-typesense.md +429 -0
- package/content/guides/core-concepts/persistent/transactions.md +61 -25
- package/content/guides/core-concepts/rest-controllers.md +12 -9
- package/content/guides/core-concepts/services.md +330 -60
- package/content/guides/get-started/5-minute-quickstart.md +15 -15
- package/content/guides/get-started/philosophy.md +36 -36
- package/content/guides/get-started/setup.md +3 -3
- package/content/guides/index.md +3 -3
- package/content/guides/migrations/redis-helpers-migration.md +177 -0
- package/content/guides/migrations/scoped-rbac-migration.md +17 -17
- package/content/guides/migrations/unified-connectors-migration.md +113 -0
- package/content/guides/reference/glossary.md +19 -12
- package/content/guides/reference/mcp-docs-server.md +22 -18
- package/content/guides/tutorials/building-a-crud-api.md +30 -33
- package/content/guides/tutorials/complete-installation.md +17 -17
- package/content/guides/tutorials/ecommerce-api.md +158 -119
- package/content/guides/tutorials/realtime-chat.md +176 -130
- package/content/guides/tutorials/testing.md +65 -523
- package/content/index.md +2 -180
- package/content/public/apple-touch-icon.png +0 -0
- package/content/public/og-image.png +0 -0
- package/content/public/site.webmanifest +11 -0
- package/content/references/base/application.md +4 -5
- package/content/references/base/bootstrapping.md +18 -5
- package/content/references/base/components.md +149 -120
- package/content/references/base/connectors.md +178 -0
- package/content/references/base/controllers.md +41 -30
- package/content/references/base/datasources.md +154 -81
- package/content/references/base/dependency-injection.md +34 -22
- package/content/references/base/filter-system/application-usage.md +17 -14
- package/content/references/base/filter-system/array-operators.md +7 -2
- package/content/references/base/filter-system/comparison-operators.md +3 -0
- package/content/references/base/filter-system/default-filter.md +89 -71
- package/content/references/base/filter-system/fields-order-pagination.md +22 -22
- package/content/references/base/filter-system/index.md +6 -3
- package/content/references/base/filter-system/json-filtering.md +20 -1
- package/content/references/base/filter-system/list-operators.md +1 -1
- package/content/references/base/filter-system/logical-operators.md +33 -1
- package/content/references/base/filter-system/null-operators.md +30 -1
- package/content/references/base/filter-system/quick-reference.md +23 -4
- package/content/references/base/filter-system/tips.md +5 -5
- package/content/references/base/filter-system/use-cases.md +12 -12
- package/content/references/base/grpc-controllers.md +13 -13
- package/content/references/base/index.md +24 -12
- package/content/references/base/middlewares.md +265 -327
- package/content/references/base/models.md +63 -49
- package/content/references/base/providers.md +136 -130
- package/content/references/base/repositories/advanced.md +59 -58
- package/content/references/base/repositories/index.md +115 -91
- package/content/references/base/repositories/mixins.md +55 -291
- package/content/references/base/repositories/relations.md +54 -64
- package/content/references/base/repositories/soft-deletable.md +31 -30
- package/content/references/base/services.md +296 -93
- package/content/references/configuration/environment-variables.md +46 -30
- package/content/references/configuration/index.md +6 -6
- package/content/references/index.md +17 -12
- package/content/references/quick-reference.md +65 -106
- package/content/references/utilities/crypto.md +65 -23
- package/content/references/utilities/index.md +3 -3
- package/content/references/utilities/jsx.md +6 -4
- package/content/references/utilities/module.md +68 -20
- package/content/references/utilities/parse.md +4 -14
- package/content/references/utilities/promise.md +9 -7
- package/content/references/utilities/schema.md +5 -3
- package/dist/mcp-server/common/guards.d.ts +8 -0
- package/dist/mcp-server/common/guards.d.ts.map +1 -0
- package/dist/mcp-server/common/guards.js +14 -0
- package/dist/mcp-server/common/guards.js.map +1 -0
- package/dist/mcp-server/common/index.d.ts +1 -0
- package/dist/mcp-server/common/index.d.ts.map +1 -1
- package/dist/mcp-server/common/index.js +1 -0
- package/dist/mcp-server/common/index.js.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +4 -2
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/helpers/github.helper.js +1 -1
- package/dist/mcp-server/index.js +7 -2
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +6 -2
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/base.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
- package/package.json +9 -9
- package/content/extensions/helpers/testing/index.md +0 -510
- package/content/references/base/middleware.md +0 -347
|
@@ -129,16 +129,16 @@ export class Application extends BaseApplication {
|
|
|
129
129
|
The component auto-registers REST endpoints for each configured backend. No injection needed in downstream code.
|
|
130
130
|
|
|
131
131
|
```
|
|
132
|
-
GET /assets/buckets
|
|
133
|
-
GET /assets/buckets/:bucketName
|
|
134
|
-
POST /assets/buckets/:bucketName
|
|
135
|
-
DELETE /assets/buckets/:bucketName
|
|
136
|
-
POST /assets/buckets/:bucketName/upload
|
|
137
|
-
GET /assets/buckets/:bucketName/objects
|
|
138
|
-
GET /assets/buckets/:bucketName/objects/:obj
|
|
139
|
-
GET /assets/buckets/:bucketName/
|
|
140
|
-
DELETE /assets/buckets/:bucketName/objects/:obj
|
|
141
|
-
PUT /assets/buckets/:bucketName/
|
|
132
|
+
GET /assets/buckets - List all buckets
|
|
133
|
+
GET /assets/buckets/:bucketName - Get bucket details (or null)
|
|
134
|
+
POST /assets/buckets/:bucketName - Create a bucket
|
|
135
|
+
DELETE /assets/buckets/:bucketName - Delete a bucket
|
|
136
|
+
POST /assets/buckets/:bucketName/upload - Upload files
|
|
137
|
+
GET /assets/buckets/:bucketName/objects - List objects in bucket
|
|
138
|
+
GET /assets/buckets/:bucketName/objects/:obj - Stream file inline
|
|
139
|
+
GET /assets/buckets/:bucketName/download/:obj - Download file (attachment)
|
|
140
|
+
DELETE /assets/buckets/:bucketName/objects/:obj - Delete file
|
|
141
|
+
PUT /assets/buckets/:bucketName/meta-links/:obj - Sync MetaLink (MetaLink only)
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
Each storage backend gets its own base path (`/assets`, `/resources`, etc.) with the same endpoint structure.
|
|
@@ -237,12 +237,12 @@ Each route can be individually configured with authentication, middleware, and p
|
|
|
237
237
|
name: 'AssetController',
|
|
238
238
|
basePath: '/assets',
|
|
239
239
|
routes: {
|
|
240
|
-
getBuckets: { authenticate: { strategies: ['jwt'], mode: '
|
|
241
|
-
upload: { authenticate: { strategies: ['jwt'], mode: '
|
|
240
|
+
getBuckets: { authenticate: { strategies: ['jwt'], mode: 'any' } },
|
|
241
|
+
upload: { authenticate: { strategies: ['jwt'], mode: 'any' }, middleware: [rateLimitMw] },
|
|
242
242
|
getObjectByName: { /* public -- no authenticate */ },
|
|
243
243
|
downloadObjectByName: { /* public */ },
|
|
244
|
-
deleteObject: { authenticate: { strategies: ['jwt'], mode: '
|
|
245
|
-
deleteBucket: { authenticate: { strategies: ['jwt'], mode: '
|
|
244
|
+
deleteObject: { authenticate: { strategies: ['jwt'], mode: 'any' } },
|
|
245
|
+
deleteBucket: { authenticate: { strategies: ['jwt'], mode: 'any' } },
|
|
246
246
|
},
|
|
247
247
|
},
|
|
248
248
|
// ...
|
|
@@ -288,11 +288,13 @@ type TStaticAssetExtraOptions = {
|
|
|
288
288
|
};
|
|
289
289
|
normalizeNameFn?: (opts: { originalName: string }) => string;
|
|
290
290
|
normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
|
|
291
|
+
/** Maximum folder nesting depth allowed in object paths. Default: 2 */
|
|
292
|
+
maxFolderDepth?: number;
|
|
291
293
|
[key: string]: any;
|
|
292
294
|
};
|
|
293
295
|
|
|
294
296
|
type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
|
|
295
|
-
model: typeof
|
|
297
|
+
model: typeof BasePostgresEntity<Schema>;
|
|
296
298
|
repository: DefaultCRUDRepository<Schema>;
|
|
297
299
|
createMetaLink?: (opts: {
|
|
298
300
|
uploadResult: IUploadResult;
|
|
@@ -393,7 +395,8 @@ MetaLink is an optional feature that tracks uploaded files in a database, storin
|
|
|
393
395
|
**1. Create Model:**
|
|
394
396
|
|
|
395
397
|
```typescript
|
|
396
|
-
import { BaseMetaLinkModel
|
|
398
|
+
import { BaseMetaLinkModel } from '@venizia/ignis/static-asset';
|
|
399
|
+
import { model } from '@venizia/ignis';
|
|
397
400
|
|
|
398
401
|
@model({ type: 'entity' })
|
|
399
402
|
export class FileMetaLinkModel extends BaseMetaLinkModel {
|
|
@@ -405,18 +408,13 @@ export class FileMetaLinkModel extends BaseMetaLinkModel {
|
|
|
405
408
|
|
|
406
409
|
```typescript
|
|
407
410
|
import { BaseMetaLinkRepository } from '@venizia/ignis/static-asset';
|
|
408
|
-
import { repository
|
|
409
|
-
import
|
|
411
|
+
import { repository } from '@venizia/ignis';
|
|
412
|
+
import { FileMetaLinkModel } from './file-meta-link.model';
|
|
413
|
+
import { PostgresDataSource } from '../datasources/postgres.datasource';
|
|
410
414
|
|
|
411
|
-
@repository({})
|
|
415
|
+
@repository({ model: FileMetaLinkModel, dataSource: PostgresDataSource })
|
|
412
416
|
export class FileMetaLinkRepository extends BaseMetaLinkRepository {
|
|
413
|
-
constructor
|
|
414
|
-
super({
|
|
415
|
-
entityClass: FileMetaLinkModel,
|
|
416
|
-
relations: {},
|
|
417
|
-
dataSource,
|
|
418
|
-
});
|
|
419
|
-
}
|
|
417
|
+
// No constructor needed - dataSource auto-injected from @repository metadata
|
|
420
418
|
}
|
|
421
419
|
```
|
|
422
420
|
|
|
@@ -427,8 +425,8 @@ The model has `skipMigrate: true`, so create the table manually:
|
|
|
427
425
|
```sql
|
|
428
426
|
CREATE TABLE "MetaLink" (
|
|
429
427
|
id TEXT PRIMARY KEY,
|
|
430
|
-
created_at
|
|
431
|
-
modified_at
|
|
428
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
429
|
+
modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
432
430
|
bucket_name TEXT NOT NULL,
|
|
433
431
|
object_name TEXT NOT NULL,
|
|
434
432
|
link TEXT NOT NULL,
|
|
@@ -15,9 +15,9 @@ The component dynamically generates REST endpoints for each configured storage b
|
|
|
15
15
|
| `POST` | <code v-pre>/{basePath}/buckets/:bucketName/upload</code> | Upload files |
|
|
16
16
|
| `GET` | <code v-pre>/{basePath}/buckets/:bucketName/objects</code> | List objects |
|
|
17
17
|
| `GET` | <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName</code> | Stream file |
|
|
18
|
-
| `GET` | <code v-pre>/{basePath}/buckets/:bucketName/
|
|
18
|
+
| `GET` | <code v-pre>/{basePath}/buckets/:bucketName/download/:objectName</code> | Download file |
|
|
19
19
|
| `DELETE` | <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName</code> | Delete object |
|
|
20
|
-
| `PUT` | <code v-pre>/{basePath}/buckets/:bucketName/
|
|
20
|
+
| `PUT` | <code v-pre>/{basePath}/buckets/:bucketName/meta-links/:objectName</code> | Sync MetaLink (MetaLink only) |
|
|
21
21
|
|
|
22
22
|
#### GET <code v-pre>/{basePath}/buckets</code>
|
|
23
23
|
**Response `200`:**
|
|
@@ -74,6 +74,7 @@ The `isDeleted` field is a boolean indicating whether the bucket was successfull
|
|
|
74
74
|
- `principalType` (optional, string): Type of the principal to associate with the uploaded files (e.g., `"user"`, `"service"`)
|
|
75
75
|
- `principalId` (optional, string or number): ID of the principal. Always coerced to a string via `String()` before storage regardless of input type
|
|
76
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
|
|
77
78
|
|
|
78
79
|
**Validation:** Bucket name validated with `isValidName()`. Returns 400 `"Invalid bucket name"` if invalid.
|
|
79
80
|
|
|
@@ -187,7 +188,7 @@ All fields in the `IObjectInfo` response are optional. The `prefix` field is pre
|
|
|
187
188
|
- `bucketName` (path): Bucket name
|
|
188
189
|
- `objectName` (path): Object name (URL-encoded)
|
|
189
190
|
|
|
190
|
-
**Validation:**
|
|
191
|
+
**Validation:** Bucket name validated with `isValidName()`; object name validated with `isValidPath()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name or path"` respectively if either is invalid.
|
|
191
192
|
|
|
192
193
|
**Response:**
|
|
193
194
|
- Streams file content with appropriate headers
|
|
@@ -196,12 +197,12 @@ All fields in the `IObjectInfo` response are optional. The `prefix` field is pre
|
|
|
196
197
|
- `X-Content-Type-Options`: `nosniff`
|
|
197
198
|
- Additional whitelisted headers forwarded from storage metadata (see [Header Sanitization](./api#header-sanitization))
|
|
198
199
|
|
|
199
|
-
#### GET <code v-pre>/{basePath}/buckets/:bucketName/
|
|
200
|
+
#### GET <code v-pre>/{basePath}/buckets/:bucketName/download/:objectName</code>
|
|
200
201
|
**Parameters:**
|
|
201
202
|
- `bucketName` (path): Bucket name
|
|
202
203
|
- `objectName` (path): Object name (URL-encoded)
|
|
203
204
|
|
|
204
|
-
**Validation:**
|
|
205
|
+
**Validation:** Bucket name validated with `isValidName()`; object name validated with `isValidPath()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name or path"` respectively if either is invalid.
|
|
205
206
|
|
|
206
207
|
**Response:**
|
|
207
208
|
- Streams file with download headers
|
|
@@ -214,7 +215,7 @@ All fields in the `IObjectInfo` response are optional. The `prefix` field is pre
|
|
|
214
215
|
|
|
215
216
|
**Example:**
|
|
216
217
|
```typescript
|
|
217
|
-
const downloadUrl = `/assets/buckets/uploads/
|
|
218
|
+
const downloadUrl = `/assets/buckets/uploads/download/${encodeURIComponent('document.pdf')}`;
|
|
218
219
|
window.open(downloadUrl, '_blank');
|
|
219
220
|
```
|
|
220
221
|
|
|
@@ -223,7 +224,7 @@ window.open(downloadUrl, '_blank');
|
|
|
223
224
|
- `bucketName` (path): Bucket name
|
|
224
225
|
- `objectName` (path): Object to delete (URL-encoded)
|
|
225
226
|
|
|
226
|
-
**Validation:**
|
|
227
|
+
**Validation:** Bucket name validated with `isValidName()`; object name validated with `isValidPath()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name or path"` respectively if either is invalid.
|
|
227
228
|
|
|
228
229
|
**Behavior:**
|
|
229
230
|
- Deletes file from storage
|
|
@@ -248,14 +249,14 @@ await fetch(`/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectNa
|
|
|
248
249
|
// MetaLink record deletion initiated (if enabled) but may complete after response
|
|
249
250
|
```
|
|
250
251
|
|
|
251
|
-
#### PUT <code v-pre>/{basePath}/buckets/:bucketName/
|
|
252
|
+
#### PUT <code v-pre>/{basePath}/buckets/:bucketName/meta-links/:objectName</code>
|
|
252
253
|
**Availability:** Only registered when `useMetaLink: true`.
|
|
253
254
|
|
|
254
255
|
**Parameters:**
|
|
255
256
|
- `bucketName` (path): Bucket name
|
|
256
257
|
- `objectName` (path): Object name (URL-encoded)
|
|
257
258
|
|
|
258
|
-
**Validation:**
|
|
259
|
+
**Validation:** Bucket name validated with `isValidName()`; object name validated with `isValidPath()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name or path"` respectively if either is invalid.
|
|
259
260
|
|
|
260
261
|
**Behavior:**
|
|
261
262
|
- Fetches current file metadata from storage via `helper.getStat()`
|
|
@@ -302,7 +303,7 @@ const bucketName = 'user-uploads';
|
|
|
302
303
|
const objectName = 'document.pdf';
|
|
303
304
|
|
|
304
305
|
const response = await fetch(
|
|
305
|
-
`/assets/buckets/${bucketName}/
|
|
306
|
+
`/assets/buckets/${bucketName}/meta-links/${encodeURIComponent(objectName)}`,
|
|
306
307
|
{ method: 'PUT' }
|
|
307
308
|
);
|
|
308
309
|
|
|
@@ -315,7 +316,7 @@ const objects = await fetch(`/assets/buckets/${bucketName}/objects`).then(r => r
|
|
|
315
316
|
|
|
316
317
|
for (const obj of objects) {
|
|
317
318
|
await fetch(
|
|
318
|
-
`/assets/buckets/${bucketName}/
|
|
319
|
+
`/assets/buckets/${bucketName}/meta-links/${encodeURIComponent(obj.name)}`,
|
|
319
320
|
{ method: 'PUT' }
|
|
320
321
|
);
|
|
321
322
|
}
|
|
@@ -345,7 +346,7 @@ async function uploadFile(file: File, principalType?: string, principalId?: stri
|
|
|
345
346
|
|
|
346
347
|
// Download file
|
|
347
348
|
function downloadFile(bucketName: string, objectName: string) {
|
|
348
|
-
const url = `/assets/buckets/${bucketName}/
|
|
349
|
+
const url = `/assets/buckets/${bucketName}/download/${encodeURIComponent(objectName)}`;
|
|
349
350
|
window.open(url, '_blank');
|
|
350
351
|
}
|
|
351
352
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Component Documentation Template
|
|
2
2
|
|
|
3
|
-
Guide for writing consistent, professional component reference docs for
|
|
3
|
+
Guide for writing consistent, professional component reference docs for IGNIS.
|
|
4
4
|
|
|
5
5
|
## Principles
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ Guide for writing consistent, professional component reference docs for Ignis.
|
|
|
14
14
|
| Tier | Structure | When to Use | Examples |
|
|
15
15
|
|------|-----------|-------------|----------|
|
|
16
16
|
| **Tier 1** | [Single page](./single-page) | 5 or fewer config options, straightforward behavior | Health Check, Request Tracker, Swagger |
|
|
17
|
-
| **Tier 2** | 4 pages | 6+ config options, multiple strategies/providers, architectural depth | Authentication, Mail, Socket.IO, WebSocket, Static Asset |
|
|
17
|
+
| **Tier 2** | 4 pages | 6+ config options, multiple strategies/providers, architectural depth | Authentication, Authorization, Mail, Socket.IO, WebSocket, Static Asset |
|
|
18
18
|
|
|
19
19
|
### Tier 1 -- Single Page
|
|
20
20
|
|
|
@@ -111,7 +111,7 @@ upgrade() fetch(req, server)
|
|
|
111
111
|
```typescript
|
|
112
112
|
interface IWebSocketEmitterOptions {
|
|
113
113
|
identifier?: string; // Default: 'WebSocketEmitter' (used as logger scope)
|
|
114
|
-
redisConnection:
|
|
114
|
+
redisConnection: IRedisHelper; // Required -- same Redis as the server(s)
|
|
115
115
|
}
|
|
116
116
|
```
|
|
117
117
|
|
|
@@ -127,7 +127,7 @@ const emitter = new WebSocketEmitter({
|
|
|
127
127
|
The constructor:
|
|
128
128
|
1. Calls `super({ scope })` with `identifier` (or `'WebSocketEmitter'` if not provided)
|
|
129
129
|
2. Validates `redisConnection` is truthy (throws `"Invalid redis connection!"` if not)
|
|
130
|
-
3. Calls `redisConnection.
|
|
130
|
+
3. Calls `redisConnection.duplicateClient()` to create an isolated pub client
|
|
131
131
|
|
|
132
132
|
### `EMITTER_SERVER_ID`
|
|
133
133
|
|
|
@@ -217,7 +217,7 @@ Reads all binding keys from the DI container and validates required ones:
|
|
|
217
217
|
| Binding | Validation | Error on Failure |
|
|
218
218
|
|---------|-----------|------------------|
|
|
219
219
|
| `SERVER_OPTIONS` | Optional, merged with `DEFAULT_SERVER_OPTIONS` via `Object.assign()` | -- |
|
|
220
|
-
| `REDIS_CONNECTION` | Must be `instanceof
|
|
220
|
+
| `REDIS_CONNECTION` | Must be `instanceof AbstractRedisHelper` | `"Invalid instance of redisConnection"` |
|
|
221
221
|
| `AUTHENTICATE_HANDLER` | Must be truthy (non-null) | `"Invalid authenticateFn to setup WebSocket server!"` |
|
|
222
222
|
| `VALIDATE_ROOM_HANDLER` | Optional, coerced `null` to `undefined` | -- |
|
|
223
223
|
| `CLIENT_CONNECTED_HANDLER` | Optional, coerced `null` to `undefined` | -- |
|
|
@@ -28,7 +28,7 @@ The server can send `error` events or close the connection under the following c
|
|
|
28
28
|
|--------|-----------|---------------|
|
|
29
29
|
| `binding()` | `application` is falsy | `"[binding] Invalid application to bind WebSocketComponent"` |
|
|
30
30
|
| `binding()` | Node.js runtime detected | `"[WebSocketComponent] Node.js runtime is not supported yet. Please use Bun runtime."` |
|
|
31
|
-
| `resolveBindings()` | `REDIS_CONNECTION` not instanceof `
|
|
31
|
+
| `resolveBindings()` | `REDIS_CONNECTION` not instanceof `AbstractRedisHelper` | `"[WebSocketComponent][resolveBindings] Invalid instance of redisConnection | Please init connection with RedisSingleHelper (single), RedisClusterHelper (cluster), or RedisSentinelHelper (sentinel)"` |
|
|
32
32
|
| `resolveBindings()` | `AUTHENTICATE_HANDLER` is falsy | `"[WebSocketComponent] Invalid authenticateFn to setup WebSocket server!"` |
|
|
33
33
|
| `registerBunHook()` | Bun server instance not available | `"[WebSocketComponent] Bun server instance not available!"` |
|
|
34
34
|
|
|
@@ -42,20 +42,20 @@ The server can send `error` events or close the connection under the following c
|
|
|
42
42
|
|
|
43
43
|
### "Invalid instance of redisConnection"
|
|
44
44
|
|
|
45
|
-
**Cause**: The value bound to `REDIS_CONNECTION` is not an instance of `
|
|
45
|
+
**Cause**: The value bound to `REDIS_CONNECTION` is not an instance of `AbstractRedisHelper` (i.e. not a `RedisSingleHelper`, `RedisClusterHelper`, or `RedisSentinelHelper`).
|
|
46
46
|
|
|
47
|
-
**Fix**: Use
|
|
47
|
+
**Fix**: Use one of the concrete topology helpers:
|
|
48
48
|
|
|
49
49
|
```typescript
|
|
50
50
|
import { WebSocketBindingKeys } from '@venizia/ignis/websocket';
|
|
51
51
|
|
|
52
52
|
// Correct
|
|
53
53
|
this.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION })
|
|
54
|
-
.toValue(new
|
|
54
|
+
.toValue(new RedisSingleHelper({ name: 'websocket', host, port, password }));
|
|
55
55
|
|
|
56
56
|
// Wrong -- raw ioredis client
|
|
57
57
|
this.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION })
|
|
58
|
-
.toValue(new Redis(6379)); // This is NOT
|
|
58
|
+
.toValue(new Redis(6379)); // This is NOT an AbstractRedisHelper!
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
### "Invalid authenticateFn to setup WebSocket server!"
|
|
@@ -92,7 +92,7 @@ import {
|
|
|
92
92
|
WebSocketBindingKeys,
|
|
93
93
|
} from '@venizia/ignis/websocket';
|
|
94
94
|
import {
|
|
95
|
-
|
|
95
|
+
RedisSingleHelper,
|
|
96
96
|
} from '@venizia/ignis-helpers';
|
|
97
97
|
import type {
|
|
98
98
|
TWebSocketAuthenticateFn,
|
|
@@ -107,7 +107,7 @@ import type {
|
|
|
107
107
|
} from '@venizia/ignis-helpers';
|
|
108
108
|
|
|
109
109
|
export class Application extends BaseApplication {
|
|
110
|
-
private redisHelper:
|
|
110
|
+
private redisHelper: RedisSingleHelper;
|
|
111
111
|
|
|
112
112
|
preConfigure(): ValueOrPromise<void> {
|
|
113
113
|
this.setupWebSocket();
|
|
@@ -116,7 +116,7 @@ export class Application extends BaseApplication {
|
|
|
116
116
|
|
|
117
117
|
setupWebSocket() {
|
|
118
118
|
// 1. Redis connection (required for cross-instance messaging)
|
|
119
|
-
this.redisHelper = new
|
|
119
|
+
this.redisHelper = new RedisSingleHelper({
|
|
120
120
|
name: 'websocket-redis',
|
|
121
121
|
host: process.env.REDIS_HOST ?? 'localhost',
|
|
122
122
|
port: +(process.env.REDIS_PORT ?? 6379),
|
|
@@ -124,7 +124,7 @@ export class Application extends BaseApplication {
|
|
|
124
124
|
autoConnect: false,
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
this.bind<
|
|
127
|
+
this.bind<RedisSingleHelper>({
|
|
128
128
|
key: WebSocketBindingKeys.REDIS_CONNECTION,
|
|
129
129
|
}).toValue(this.redisHelper);
|
|
130
130
|
|
|
@@ -331,7 +331,7 @@ interface IServerOptions {
|
|
|
331
331
|
| Binding Key | Constant | Type | Required | Default |
|
|
332
332
|
|------------|----------|------|----------|---------|
|
|
333
333
|
| `@app/websocket/server-options` | `WebSocketBindingKeys.SERVER_OPTIONS` | `Partial<IServerOptions>` | No | See [Configuration](#configuration) |
|
|
334
|
-
| `@app/websocket/redis-connection` | `WebSocketBindingKeys.REDIS_CONNECTION` | `
|
|
334
|
+
| `@app/websocket/redis-connection` | `WebSocketBindingKeys.REDIS_CONNECTION` | `IRedisHelper` | **Yes** | `null` |
|
|
335
335
|
| `@app/websocket/authenticate-handler` | `WebSocketBindingKeys.AUTHENTICATE_HANDLER` | `TWebSocketAuthenticateFn` | **Yes** | `null` |
|
|
336
336
|
| `@app/websocket/validate-room-handler` | `WebSocketBindingKeys.VALIDATE_ROOM_HANDLER` | `TWebSocketValidateRoomFn` | No | `null` |
|
|
337
337
|
| `@app/websocket/client-connected-handler` | `WebSocketBindingKeys.CLIENT_CONNECTED_HANDLER` | `TWebSocketClientConnectedFn` | No | `null` |
|
|
@@ -109,10 +109,10 @@ It connects to Redis and publishes messages using the same `IRedisSocketMessage`
|
|
|
109
109
|
#### Emitter Setup
|
|
110
110
|
|
|
111
111
|
```typescript
|
|
112
|
-
import { WebSocketEmitter,
|
|
112
|
+
import { WebSocketEmitter, RedisSingleHelper } from '@venizia/ignis-helpers';
|
|
113
113
|
|
|
114
114
|
// 1. Create a Redis connection (same Redis instance as the WebSocket server)
|
|
115
|
-
const redisHelper = new
|
|
115
|
+
const redisHelper = new RedisSingleHelper({
|
|
116
116
|
name: 'emitter-redis',
|
|
117
117
|
host: process.env.REDIS_HOST ?? 'localhost',
|
|
118
118
|
port: +(process.env.REDIS_PORT ?? 6379),
|
|
@@ -343,7 +343,7 @@ Both `WebSocketServerHelper` and `WebSocketEmitter` support Redis single instanc
|
|
|
343
343
|
type TRedisClient = Redis | Cluster;
|
|
344
344
|
```
|
|
345
345
|
|
|
346
|
-
The Redis client is obtained via `redisConnection.
|
|
346
|
+
The Redis client is obtained via `redisConnection.duplicateClient()`. This creates a fresh connection that inherits the parent's configuration (including cluster mode). This ensures WebSocket pub/sub traffic does not interfere with application Redis usage.
|
|
347
347
|
|
|
348
348
|
### Subscription Setup
|
|
349
349
|
|
|
@@ -147,7 +147,7 @@ hourlyJob.start();
|
|
|
147
147
|
|
|
148
148
|
### Accessing the Underlying CronJob
|
|
149
149
|
|
|
150
|
-
The `instance` property exposes the underlying `CronJob` from the `cron` package, giving access to the full API (e.g., `stop()`, `
|
|
150
|
+
The `instance` property exposes the underlying `CronJob` from the `cron` package, giving access to the full API (e.g., `stop()`, `isActive`, `lastDate()`).
|
|
151
151
|
|
|
152
152
|
```typescript
|
|
153
153
|
const job = new CronHelper({
|
|
@@ -158,7 +158,7 @@ const job = new CronHelper({
|
|
|
158
158
|
job.start();
|
|
159
159
|
|
|
160
160
|
// Access the underlying CronJob directly
|
|
161
|
-
console.log(job.instance.
|
|
161
|
+
console.log(job.instance.isActive); // true
|
|
162
162
|
job.instance.stop();
|
|
163
163
|
```
|
|
164
164
|
|
|
@@ -18,7 +18,7 @@ Cryptographic utilities for AES symmetric encryption, RSA asymmetric encryption,
|
|
|
18
18
|
| Type | Symmetric | Asymmetric | Asymmetric + Symmetric |
|
|
19
19
|
| Key exchange | Shared secret | Public/private | Diffie-Hellman |
|
|
20
20
|
| Speed | Fast | Slow (large keys) | Fast (small keys) |
|
|
21
|
-
| Max message | Unlimited | ~
|
|
21
|
+
| Max message | Unlimited | ~214 bytes (2048-bit, OAEP) | Unlimited |
|
|
22
22
|
| Async | No | No | Yes (Web Crypto) |
|
|
23
23
|
| Runtime | Node.js `crypto` | Node.js `crypto` | `crypto.subtle` (Bun/Browser) |
|
|
24
24
|
|
|
@@ -8,7 +8,7 @@ Structured access to application environment variables with prefix filtering, ty
|
|
|
8
8
|
|------|-------|
|
|
9
9
|
| **Package** | `@venizia/ignis-helpers` |
|
|
10
10
|
| **Classes** | `ApplicationEnvironment`, `Environment` |
|
|
11
|
-
| **
|
|
11
|
+
| **Implements** | `IApplicationEnvironment` (interface) |
|
|
12
12
|
| **Singleton** | `applicationEnvironment` (alias `Envs`) -- auto-initialized at module load |
|
|
13
13
|
| **Runtimes** | Both |
|
|
14
14
|
|
|
@@ -129,17 +129,32 @@ if (Environment.is({ name: 'staging' })) {
|
|
|
129
129
|
|
|
130
130
|
#### Available Stages
|
|
131
131
|
|
|
132
|
-
| Constant | Value |
|
|
133
|
-
|
|
134
|
-
| `Environment.LOCAL` | `'local'` |
|
|
135
|
-
| `Environment.DEBUG` | `'debug'` |
|
|
136
|
-
| `Environment.DEVELOPMENT` | `'development'` |
|
|
137
|
-
| `Environment.
|
|
138
|
-
| `Environment.
|
|
139
|
-
| `Environment.
|
|
140
|
-
| `Environment.
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
| Constant | Value | Development stage |
|
|
133
|
+
|----------|-------|-------------------|
|
|
134
|
+
| `Environment.LOCAL` | `'local'` | yes |
|
|
135
|
+
| `Environment.DEBUG` | `'debug'` | yes |
|
|
136
|
+
| `Environment.DEVELOPMENT` | `'development'` | yes |
|
|
137
|
+
| `Environment.DEV` | `'dev'` | yes - the short spelling of `development` |
|
|
138
|
+
| `Environment.SIT` | `'sit'` | yes |
|
|
139
|
+
| `Environment.UAT` | `'uat'` | no |
|
|
140
|
+
| `Environment.ALPHA` | `'alpha'` | no |
|
|
141
|
+
| `Environment.BETA` | `'beta'` | no |
|
|
142
|
+
| `Environment.STAGING` | `'staging'` | no |
|
|
143
|
+
| `Environment.PRODUCTION` | `'production'` | no |
|
|
144
|
+
|
|
145
|
+
All stages are collected in `Environment.COMMON_ENVS` (a `Set<string>`), which is used internally by the Logger to determine whether debug logging should be active. A `NODE_ENV` outside this set silences `DEBUG=true` entirely.
|
|
146
|
+
|
|
147
|
+
#### `Environment.DEVELOPMENT_ENVS` - the error-detail boundary
|
|
148
|
+
|
|
149
|
+
The stages marked "development stage" above form `Environment.DEVELOPMENT_ENVS`. IGNIS's error handler consults this set to decide whether an error response may carry internal detail - a stack trace, a SQL constraint name, a raw driver message.
|
|
150
|
+
|
|
151
|
+
The rule is fail-closed. A leak is opt-in by an explicit development name, so **anything else is sanitized as production**, including:
|
|
152
|
+
|
|
153
|
+
- `alpha`, `beta`, `uat`, `staging` - real users reach these
|
|
154
|
+
- an unrecognized name (a typo, a stage nobody added to the set)
|
|
155
|
+
- `NODE_ENV` left unset
|
|
156
|
+
|
|
157
|
+
Running a local service under `NODE_ENV=alpha` therefore gives you the same stripped-down error responses your users see. If you want the details while developing, set `NODE_ENV` to `development`, `dev`, or `local`.
|
|
143
158
|
|
|
144
159
|
### Configuring the Prefix
|
|
145
160
|
|
|
@@ -14,62 +14,108 @@ Standardized error class and factory for throwing HTTP-aware errors with machine
|
|
|
14
14
|
#### Import Paths
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
|
-
import { ApplicationError, getError } from '@venizia/ignis-helpers';
|
|
17
|
+
import { ApplicationError, getError, isApplicationError } from '@venizia/ignis-helpers';
|
|
18
18
|
import { ErrorSchema } from '@venizia/ignis-helpers';
|
|
19
19
|
import type { TError } from '@venizia/ignis-helpers';
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Creating an Instance
|
|
23
23
|
|
|
24
|
-
`ApplicationError` extends the native `Error` class with an HTTP `statusCode` and an optional `messageCode` for machine-readable error identification.
|
|
24
|
+
The canonical way to raise an error in IGNIS is the standalone `getError()` factory - use it everywhere (house rule: `getError`, never `new Error`). `ApplicationError` extends the native `Error` class with an HTTP `statusCode` and an optional `messageCode` for machine-readable error identification; `getError()` constructs one for you.
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
|
-
import {
|
|
27
|
+
import { getError, HTTP } from '@venizia/ignis-helpers';
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
throw getError({
|
|
30
30
|
message: 'User not found',
|
|
31
31
|
statusCode: HTTP.ResultCodes.RS_4.NotFound,
|
|
32
|
-
messageCode: '
|
|
32
|
+
messageCode: 'core.user.not_found',
|
|
33
33
|
});
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
####
|
|
36
|
+
#### Options (`TError`)
|
|
37
|
+
|
|
38
|
+
The same options apply to `getError()`, the `ApplicationError` constructor, and the static factory:
|
|
37
39
|
|
|
38
40
|
| Option | Type | Default | Description |
|
|
39
41
|
|--------|------|---------|-------------|
|
|
40
42
|
| `message` | `string` | -- (required) | Human-readable error message |
|
|
41
43
|
| `statusCode` | `number` | `400` | HTTP status code |
|
|
42
|
-
| `messageCode` | `string` | `
|
|
43
|
-
| `name` | `string` | `undefined` | Error name |
|
|
44
|
+
| `messageCode` | `string` | `MessageCode.DEFAULT` (`'core.system_error'`) | Machine-readable error code for client-side handling. Always resolved through `MessageCode.resolve()` -- never left `undefined`, and lower-cased regardless of what was passed in |
|
|
45
|
+
| `name` | `string` | `undefined` | Accepted by the schema but discarded by the constructor (the native `Error` name is kept) |
|
|
44
46
|
|
|
45
47
|
> [!TIP]
|
|
46
|
-
> The `TError` type is derived from `ErrorSchema` (a Zod schema) and uses `.catchall(z.any())`, so you can pass additional arbitrary properties beyond the four listed above.
|
|
48
|
+
> The `TError` type is derived from `ErrorSchema` (a Zod schema) and uses `.catchall(z.any())`, so you can pass additional arbitrary properties beyond the four listed above. Extra properties are collected into the `extra` field on the resulting `ApplicationError` instance.
|
|
47
49
|
|
|
48
|
-
#### `
|
|
50
|
+
#### `ApplicationError` Constructor and Static Factory
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
`getError()` is the canonical form. The class constructor and the static `ApplicationError.getError()` are legal equivalents - use them only when a direct class reference reads better:
|
|
51
53
|
|
|
52
54
|
```typescript
|
|
53
|
-
import {
|
|
55
|
+
import { ApplicationError, HTTP } from '@venizia/ignis-helpers';
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
// Class constructor (equivalent to getError)
|
|
58
|
+
const error = new ApplicationError({
|
|
59
|
+
message: 'Configuration missing',
|
|
60
|
+
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Static factory (equivalent to getError)
|
|
64
|
+
throw ApplicationError.getError({
|
|
56
65
|
message: 'Invalid credentials',
|
|
57
66
|
statusCode: HTTP.ResultCodes.RS_4.Unauthorized,
|
|
58
|
-
messageCode: '
|
|
67
|
+
messageCode: 'core.auth.invalid_credentials',
|
|
59
68
|
});
|
|
60
69
|
```
|
|
61
70
|
|
|
62
|
-
|
|
71
|
+
### MessageCode
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
`MessageCode` builds and normalizes the machine-readable codes carried on `ApplicationError.messageCode`. Every code an application throws should be constructed with `MessageCode.build()` rather than typed as a raw string literal -- a malformed code fails at module load (import time) instead of shipping dead into production.
|
|
65
74
|
|
|
66
75
|
```typescript
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
76
|
+
import { MessageCode } from '@venizia/ignis-helpers';
|
|
77
|
+
|
|
78
|
+
export class UserErrorCodes {
|
|
79
|
+
static readonly NOT_FOUND = MessageCode.build({ parts: ['core', 'user', 'not_found'] });
|
|
80
|
+
static readonly DUPLICATE_EMAIL = MessageCode.build({ parts: ['core', 'user', 'duplicate_email'] });
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Member | Type | Value / Signature | Description |
|
|
85
|
+
|--------|------|--------------------|-------------|
|
|
86
|
+
| `DEFAULT` | `string` | `'core.system_error'` | The code `ApplicationError` falls back to when no `messageCode` is supplied |
|
|
87
|
+
| `SEPARATOR` | `string` | `'.'` | Joins segments into a dotted code (e.g. `core.mail.send_failed`) |
|
|
88
|
+
| `SEGMENT_PATTERN` | `RegExp` | `/^[a-z0-9]+(_[a-z0-9]+)*$/` | Each segment must be lower snake_case -- `a-z`, `0-9`, `_` |
|
|
89
|
+
| `MIN_SEGMENTS` | `number` | `2` | A code needs at least a namespace and a reason (e.g. `core.not_found` is valid, `not_found` alone is not) |
|
|
90
|
+
| `build(opts: { parts: Array<string> })` | `string` | -- | Joins `parts` with `SEPARATOR` and lower-cases the result. Throws (via `getError`) if `parts.length < MIN_SEGMENTS`, or if any segment fails `SEGMENT_PATTERN` |
|
|
91
|
+
| `isValid(code: string)` | `boolean` | -- | Cheap structural check for a code arriving from outside the process |
|
|
92
|
+
| `resolve(code?: string)` | `string` | -- | Normalizes an absent or empty code to `DEFAULT`; otherwise lower-cases `code` |
|
|
93
|
+
|
|
94
|
+
> [!IMPORTANT]
|
|
95
|
+
> `ApplicationError`'s constructor always calls `MessageCode.resolve(messageCode)` -- so `error.messageCode` is **never** `undefined`, and it is **always lower-cased** regardless of the casing passed to `getError()`. A comparison like `error.messageCode === 'DUPLICATE_EMAIL'` is always false; compare against `'duplicate_email'` (or the exact string returned by `MessageCode.build()`).
|
|
96
|
+
|
|
97
|
+
## Checking for an Application Error
|
|
98
|
+
|
|
99
|
+
Use `isApplicationError(error)` to recognize an application error by **shape** - an `Error` instance carrying a numeric `statusCode` - rather than by class identity:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { isApplicationError } from '@venizia/ignis-helpers';
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await someOperation();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (isApplicationError(error)) {
|
|
108
|
+
// already shaped (has a statusCode) - surface as-is
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
// an unknown failure - sanitize before rethrowing
|
|
112
|
+
throw getError({ message: 'Operation failed', statusCode: HTTP.ResultCodes.RS_5.InternalServerError });
|
|
113
|
+
}
|
|
71
114
|
```
|
|
72
115
|
|
|
116
|
+
> [!WARNING]
|
|
117
|
+
> Never compare `ApplicationError` with `instanceof` across a package boundary. `inversion` ships **dual CJS + ESM** builds (its DI powers frontend libraries), so its error class deliberately has more than one runtime identity, and `helpers` keeps its own `ApplicationError` for the backend stack. Two objects that are both "an application error" can be instances of different classes, so `instanceof` gives false negatives. `isApplicationError` checks the shape and works regardless of which package threw. The search connectors use it to decide what is already shaped versus what must be sanitized as a `503`.
|
|
118
|
+
|
|
73
119
|
## Usage
|
|
74
120
|
|
|
75
121
|
### Throwing Errors in Services
|
|
@@ -109,23 +155,33 @@ throw getError({
|
|
|
109
155
|
|
|
110
156
|
The built-in `appErrorHandler` middleware (from `@venizia/ignis`) catches `ApplicationError` instances and formats them into consistent JSON responses. The response shape differs by environment.
|
|
111
157
|
|
|
112
|
-
#### Production Response
|
|
158
|
+
#### Sanitized (Production-Class) Response
|
|
159
|
+
|
|
160
|
+
The handler is **fail-closed**: it exposes internals only when `NODE_ENV` names a development environment - one of `local`, `debug`, `development`, `dev`, `sit` (`Environment.DEVELOPMENT_ENVS`). Everything else is sanitized as production: `production`, and also `alpha`, `beta`, `uat`, `staging`, an unrecognized name, and an unset `NODE_ENV`.
|
|
161
|
+
|
|
162
|
+
In a sanitized response, `stack` and `cause` are omitted from `details`. For unexpected errors without a `statusCode` (i.e., not thrown via `getError`), the raw message is replaced with a generic `"Internal Server Error"`, and a database error keeps only its base message - no SQL detail, no table or constraint name.
|
|
113
163
|
|
|
114
164
|
```json
|
|
115
165
|
{
|
|
116
166
|
"message": "User not found",
|
|
167
|
+
"messageCode": "core.user.not_found",
|
|
117
168
|
"statusCode": 404,
|
|
118
|
-
"requestId": "abc-123-def"
|
|
169
|
+
"requestId": "abc-123-def",
|
|
170
|
+
"details": {
|
|
171
|
+
"url": "http://localhost:3000/api/users/123",
|
|
172
|
+
"path": "/api/users/123"
|
|
173
|
+
}
|
|
119
174
|
}
|
|
120
175
|
```
|
|
121
176
|
|
|
122
177
|
#### Development Response
|
|
123
178
|
|
|
124
|
-
In development (
|
|
179
|
+
In a development environment (see the list above), `details` additionally includes debugging fields:
|
|
125
180
|
|
|
126
181
|
```json
|
|
127
182
|
{
|
|
128
183
|
"message": "User not found",
|
|
184
|
+
"messageCode": "core.user.not_found",
|
|
129
185
|
"statusCode": 404,
|
|
130
186
|
"requestId": "abc-123-def",
|
|
131
187
|
"details": {
|
|
@@ -197,9 +253,9 @@ throw getError({
|
|
|
197
253
|
|
|
198
254
|
### Error response missing `stack` and `cause`
|
|
199
255
|
|
|
200
|
-
**Cause:**
|
|
256
|
+
**Cause:** `NODE_ENV` is not one of the development environments, so the handler sanitized the response (`url` and `path` are always included). Note this is what happens on `alpha`/`staging`, on a typo'd name, and when `NODE_ENV` is unset - not only on `production`.
|
|
201
257
|
|
|
202
|
-
**Fix:** Set `NODE_ENV
|
|
258
|
+
**Fix:** Set `NODE_ENV` to a development name - `development`, `dev`, `local`, `debug` or `sit`. Anything else stays sanitized by design.
|
|
203
259
|
|
|
204
260
|
### Errors returning 500 instead of expected status code
|
|
205
261
|
|