@venizia/ignis-docs 0.0.4-1 → 0.0.4-2
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/package.json +1 -1
- package/wiki/best-practices/api-usage-examples.md +1 -0
- package/wiki/best-practices/code-style-standards/advanced-patterns.md +259 -0
- package/wiki/best-practices/code-style-standards/constants-configuration.md +225 -0
- package/wiki/best-practices/code-style-standards/control-flow.md +245 -0
- package/wiki/best-practices/code-style-standards/documentation.md +221 -0
- package/wiki/best-practices/code-style-standards/function-patterns.md +142 -0
- package/wiki/best-practices/code-style-standards/index.md +110 -0
- package/wiki/best-practices/code-style-standards/naming-conventions.md +174 -0
- package/wiki/best-practices/code-style-standards/route-definitions.md +150 -0
- package/wiki/best-practices/code-style-standards/tooling.md +155 -0
- package/wiki/best-practices/code-style-standards/type-safety.md +165 -0
- package/wiki/best-practices/common-pitfalls.md +164 -3
- package/wiki/best-practices/contribution-workflow.md +1 -1
- package/wiki/best-practices/data-modeling.md +102 -2
- package/wiki/best-practices/error-handling.md +468 -0
- package/wiki/best-practices/index.md +204 -21
- package/wiki/best-practices/performance-optimization.md +180 -0
- package/wiki/best-practices/security-guidelines.md +249 -0
- package/wiki/best-practices/testing-strategies.md +620 -0
- package/wiki/changelogs/2026-01-05-range-queries-content-range.md +184 -0
- package/wiki/changelogs/2026-01-06-basic-authentication.md +103 -0
- package/wiki/changelogs/2026-01-07-controller-route-customization.md +209 -0
- package/wiki/changelogs/index.md +3 -0
- package/wiki/guides/core-concepts/components-guide.md +1 -1
- package/wiki/guides/core-concepts/persistent/models.md +10 -0
- package/wiki/guides/tutorials/complete-installation.md +1 -1
- package/wiki/guides/tutorials/testing.md +1 -1
- package/wiki/references/base/components.md +47 -29
- package/wiki/references/base/controllers.md +215 -18
- package/wiki/references/base/filter-system/fields-order-pagination.md +84 -0
- package/wiki/references/base/middlewares.md +33 -1
- package/wiki/references/base/models.md +40 -2
- package/wiki/references/base/repositories/index.md +2 -0
- package/wiki/references/components/authentication.md +261 -247
- package/wiki/references/helpers/index.md +1 -1
- package/wiki/references/src-details/core.md +1 -1
- package/wiki/best-practices/code-style-standards.md +0 -1193
|
@@ -329,29 +329,42 @@ This factory method returns a `BaseController` class that is already set up with
|
|
|
329
329
|
|
|
330
330
|
### Routes Configuration
|
|
331
331
|
|
|
332
|
-
The `routes` option provides a unified way to configure
|
|
332
|
+
The `routes` option provides a unified way to configure request/response schemas and authentication for each endpoint:
|
|
333
333
|
|
|
334
334
|
```typescript
|
|
335
335
|
type TRouteAuthConfig =
|
|
336
336
|
| { skipAuth: true }
|
|
337
337
|
| { skipAuth?: false; authStrategies: Array<TAuthStrategy> };
|
|
338
338
|
|
|
339
|
-
type
|
|
340
|
-
|
|
341
|
-
|
|
339
|
+
type TRequestConfig = {
|
|
340
|
+
query?: z.ZodObject; // Custom query parameters
|
|
341
|
+
headers?: z.ZodObject; // Custom headers
|
|
342
|
+
params?: z.ZodObject; // Custom path parameters
|
|
343
|
+
body?: z.ZodObject; // Custom request body (write routes only)
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
type TResponseConfig = {
|
|
347
|
+
schema?: z.ZodObject; // Custom response body schema
|
|
348
|
+
headers?: z.ZodObject; // Custom response headers
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
type TBaseRouteConfig = TRouteAuthConfig & {
|
|
352
|
+
request?: TRequestConfig;
|
|
353
|
+
response?: TResponseConfig;
|
|
354
|
+
};
|
|
342
355
|
```
|
|
343
356
|
|
|
344
|
-
| Route |
|
|
357
|
+
| Route | Customizable Components | Description |
|
|
345
358
|
| :--- | :--- | :--- |
|
|
346
|
-
| `count` |
|
|
347
|
-
| `find` |
|
|
348
|
-
| `findOne` |
|
|
349
|
-
| `findById` |
|
|
350
|
-
| `create` |
|
|
351
|
-
| `updateById` |
|
|
352
|
-
| `updateBy` |
|
|
353
|
-
| `deleteById` |
|
|
354
|
-
| `deleteBy` |
|
|
359
|
+
| `count` | query, headers, response | Config for count endpoint |
|
|
360
|
+
| `find` | query, headers, response | Config for find endpoint |
|
|
361
|
+
| `findOne` | query, headers, response | Config for findOne endpoint |
|
|
362
|
+
| `findById` | query, headers, params, response | Config for findById endpoint |
|
|
363
|
+
| `create` | headers, body, response | Config for create endpoint |
|
|
364
|
+
| `updateById` | headers, params, body, response | Config for updateById endpoint |
|
|
365
|
+
| `updateBy` | query, headers, body, response | Config for updateBy endpoint |
|
|
366
|
+
| `deleteById` | headers, params, response | Config for deleteById endpoint |
|
|
367
|
+
| `deleteBy` | query, headers, response | Config for deleteBy endpoint |
|
|
355
368
|
|
|
356
369
|
### Auth Resolution Priority
|
|
357
370
|
|
|
@@ -397,17 +410,70 @@ const ArticleController = ControllerFactory.defineCrudController({
|
|
|
397
410
|
},
|
|
398
411
|
});
|
|
399
412
|
|
|
400
|
-
// 4. Custom
|
|
413
|
+
// 4. Custom request/response schemas with auth configuration
|
|
401
414
|
const OrderController = ControllerFactory.defineCrudController({
|
|
402
415
|
entity: OrderEntity,
|
|
403
416
|
repository: { name: 'OrderRepository' },
|
|
404
417
|
controller: { name: 'OrderController', basePath: '/orders' },
|
|
405
418
|
authStrategies: ['jwt'],
|
|
406
419
|
routes: {
|
|
407
|
-
find: {
|
|
420
|
+
find: {
|
|
421
|
+
skipAuth: true,
|
|
422
|
+
response: { schema: CustomOrderListSchema },
|
|
423
|
+
},
|
|
424
|
+
create: {
|
|
425
|
+
request: { body: CustomOrderCreateSchema },
|
|
426
|
+
response: { schema: CustomOrderResponseSchema },
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Route Customization Examples
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
import { z } from '@hono/zod-openapi';
|
|
436
|
+
|
|
437
|
+
// Custom request body for create
|
|
438
|
+
const CreateUserSchema = z.object({
|
|
439
|
+
name: z.string().min(1).max(100),
|
|
440
|
+
email: z.string().email(),
|
|
441
|
+
role: z.enum(['admin', 'user']).default('user'),
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Custom response schema (omit sensitive fields)
|
|
445
|
+
const PublicUserSchema = z.object({
|
|
446
|
+
id: z.string(),
|
|
447
|
+
name: z.string(),
|
|
448
|
+
role: z.string(),
|
|
449
|
+
createdAt: z.string(),
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const UserController = ControllerFactory.defineCrudController({
|
|
453
|
+
entity: UserEntity,
|
|
454
|
+
repository: { name: 'UserRepository' },
|
|
455
|
+
controller: { name: 'UserController', basePath: '/users' },
|
|
456
|
+
authStrategies: ['jwt'],
|
|
457
|
+
routes: {
|
|
458
|
+
// Public read endpoints
|
|
459
|
+
find: {
|
|
460
|
+
skipAuth: true,
|
|
461
|
+
response: { schema: z.array(PublicUserSchema) },
|
|
462
|
+
},
|
|
463
|
+
findById: {
|
|
464
|
+
skipAuth: true,
|
|
465
|
+
response: { schema: PublicUserSchema },
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
// Custom create with custom body and response
|
|
408
469
|
create: {
|
|
409
|
-
|
|
410
|
-
|
|
470
|
+
request: { body: CreateUserSchema },
|
|
471
|
+
response: { schema: PublicUserSchema },
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
// Delete requires JWT auth (uses default schema)
|
|
475
|
+
deleteById: {
|
|
476
|
+
authStrategies: ['jwt'],
|
|
411
477
|
},
|
|
412
478
|
},
|
|
413
479
|
});
|
|
@@ -459,6 +525,137 @@ export class ConfigurationController extends _ConfigurationController {
|
|
|
459
525
|
|
|
460
526
|
By leveraging these structured configuration options and the `ControllerFactory`, you ensure that your API is not only functional but also well-documented, easy to validate, and rapidly deployable for standard CRUD operations.
|
|
461
527
|
|
|
528
|
+
### Overriding CRUD Methods with Strong Typing
|
|
529
|
+
|
|
530
|
+
When extending a generated CRUD controller, you can override methods with full type safety using the helper types `THandlerContext` and `TInferSchema`.
|
|
531
|
+
|
|
532
|
+
#### Helper Types
|
|
533
|
+
|
|
534
|
+
| Type | Purpose |
|
|
535
|
+
| :--- | :--- |
|
|
536
|
+
| `THandlerContext<Definitions, Key>` | Extracts the strongly-typed Hono context for a specific route |
|
|
537
|
+
| `TInferSchema<ZodSchema>` | Extracts TypeScript type from a Zod schema |
|
|
538
|
+
|
|
539
|
+
#### Example: Full Controller Override Pattern
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
import { Configuration } from '@/models';
|
|
543
|
+
import { ConfigurationRepository } from '@/repositories';
|
|
544
|
+
import {
|
|
545
|
+
Authentication,
|
|
546
|
+
BindingKeys,
|
|
547
|
+
BindingNamespaces,
|
|
548
|
+
controller,
|
|
549
|
+
ControllerFactory,
|
|
550
|
+
inject,
|
|
551
|
+
THandlerContext,
|
|
552
|
+
TInferSchema,
|
|
553
|
+
} from '@venizia/ignis';
|
|
554
|
+
import { z } from '@hono/zod-openapi';
|
|
555
|
+
|
|
556
|
+
const BASE_PATH = '/configurations';
|
|
557
|
+
|
|
558
|
+
// Custom request body schema
|
|
559
|
+
const CreateConfigurationSchema = z.object({
|
|
560
|
+
code: z.string().min(1).max(100),
|
|
561
|
+
description: z.string().max(500).optional(),
|
|
562
|
+
group: z.string().min(1).max(50),
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Custom response schema
|
|
566
|
+
const CreateResponseSchema = z.object({
|
|
567
|
+
id: z.string(),
|
|
568
|
+
code: z.string(),
|
|
569
|
+
message: z.string(),
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Define the CRUD controller with custom schemas
|
|
573
|
+
const _Controller = ControllerFactory.defineCrudController({
|
|
574
|
+
repository: { name: ConfigurationRepository.name },
|
|
575
|
+
controller: { name: 'ConfigurationController', basePath: BASE_PATH },
|
|
576
|
+
authStrategies: [Authentication.STRATEGY_JWT],
|
|
577
|
+
entity: () => Configuration,
|
|
578
|
+
routes: {
|
|
579
|
+
count: { skipAuth: true },
|
|
580
|
+
create: {
|
|
581
|
+
request: { body: CreateConfigurationSchema },
|
|
582
|
+
response: { schema: CreateResponseSchema },
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Extract definitions type for method overrides
|
|
588
|
+
type TRouteDefinitions = InstanceType<typeof _Controller>['definitions'];
|
|
589
|
+
|
|
590
|
+
@controller({ path: BASE_PATH })
|
|
591
|
+
export class ConfigurationController extends _Controller {
|
|
592
|
+
constructor(
|
|
593
|
+
@inject({
|
|
594
|
+
key: BindingKeys.build({
|
|
595
|
+
namespace: BindingNamespaces.REPOSITORY,
|
|
596
|
+
key: ConfigurationRepository.name,
|
|
597
|
+
}),
|
|
598
|
+
})
|
|
599
|
+
repository: ConfigurationRepository,
|
|
600
|
+
) {
|
|
601
|
+
super(repository);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Override with full type safety
|
|
605
|
+
override async create(opts: { context: THandlerContext<TRouteDefinitions, 'CREATE'> }) {
|
|
606
|
+
const { context } = opts;
|
|
607
|
+
|
|
608
|
+
// Get typed request body using TInferSchema
|
|
609
|
+
const data = context.req.valid('json') as TInferSchema<typeof CreateConfigurationSchema>;
|
|
610
|
+
|
|
611
|
+
// Access typed properties
|
|
612
|
+
this.logger.info('[create] code: %s, group: %s', data.code, data.group);
|
|
613
|
+
|
|
614
|
+
// Custom business logic here...
|
|
615
|
+
|
|
616
|
+
// Call parent or return custom response
|
|
617
|
+
return super.create(opts);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Override updateById
|
|
621
|
+
override async updateById(opts: { context: THandlerContext<TRouteDefinitions, 'UPDATE_BY_ID'> }) {
|
|
622
|
+
const { context } = opts;
|
|
623
|
+
const { id } = context.req.valid('param');
|
|
624
|
+
const data = context.req.valid('json');
|
|
625
|
+
|
|
626
|
+
this.logger.info('[updateById] id: %s', id);
|
|
627
|
+
|
|
628
|
+
return super.updateById(opts);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Override deleteById with audit logging
|
|
632
|
+
override async deleteById(opts: { context: THandlerContext<TRouteDefinitions, 'DELETE_BY_ID'> }) {
|
|
633
|
+
const { context } = opts;
|
|
634
|
+
const { id } = context.req.valid('param');
|
|
635
|
+
|
|
636
|
+
this.logger.warn('[deleteById] Deleting id: %s', id);
|
|
637
|
+
|
|
638
|
+
return super.deleteById(opts);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
#### Available Route Keys
|
|
644
|
+
|
|
645
|
+
Use these keys with `THandlerContext`:
|
|
646
|
+
|
|
647
|
+
| Key | Method | Path |
|
|
648
|
+
| :--- | :--- | :--- |
|
|
649
|
+
| `'COUNT'` | GET | /count |
|
|
650
|
+
| `'FIND'` | GET | / |
|
|
651
|
+
| `'FIND_BY_ID'` | GET | /:id |
|
|
652
|
+
| `'FIND_ONE'` | GET | /find-one |
|
|
653
|
+
| `'CREATE'` | POST | / |
|
|
654
|
+
| `'UPDATE_BY_ID'` | PATCH | /:id |
|
|
655
|
+
| `'UPDATE_BY'` | PATCH | / |
|
|
656
|
+
| `'DELETE_BY_ID'` | DELETE | /:id |
|
|
657
|
+
| `'DELETE_BY'` | DELETE | / |
|
|
658
|
+
|
|
462
659
|
---
|
|
463
660
|
|
|
464
661
|
## See Also
|
|
@@ -140,6 +140,72 @@ const filter = {
|
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
## Range Queries (Content-Range Header)
|
|
144
|
+
|
|
145
|
+
When building paginated APIs, you often need to return the total count alongside the data for pagination UI. Use `shouldQueryRange: true` to get range information following the HTTP Content-Range standard.
|
|
146
|
+
|
|
147
|
+
### Basic Usage
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const result = await repo.find({
|
|
151
|
+
filter: { limit: 10, skip: 20 },
|
|
152
|
+
options: { shouldQueryRange: true }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Result structure:
|
|
156
|
+
// {
|
|
157
|
+
// data: [...], // Array of records
|
|
158
|
+
// range: {
|
|
159
|
+
// start: 20, // Starting index (inclusive)
|
|
160
|
+
// end: 29, // Ending index (inclusive)
|
|
161
|
+
// total: 100 // Total matching records
|
|
162
|
+
// }
|
|
163
|
+
// }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Setting HTTP Headers
|
|
167
|
+
|
|
168
|
+
Use the range information to set standard HTTP headers:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const { data, range } = await repo.find({
|
|
172
|
+
filter: { limit: 10, skip: 20, where: { status: 'active' } },
|
|
173
|
+
options: { shouldQueryRange: true }
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Format: "records start-end/total"
|
|
177
|
+
const contentRange = data.length > 0
|
|
178
|
+
? `records ${range.start}-${range.end}/${range.total}`
|
|
179
|
+
: `records */${range.total}`;
|
|
180
|
+
|
|
181
|
+
res.setHeader('Content-Range', contentRange);
|
|
182
|
+
// → "records 20-29/100"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### TDataRange Type
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
type TDataRange = {
|
|
189
|
+
start: number; // Starting index (0-based, inclusive)
|
|
190
|
+
end: number; // Ending index (0-based, inclusive)
|
|
191
|
+
total: number; // Total count matching the query
|
|
192
|
+
};
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Content-Range Format Reference
|
|
196
|
+
|
|
197
|
+
| Scenario | Content-Range Header |
|
|
198
|
+
|----------|---------------------|
|
|
199
|
+
| Items 0-9 of 100 | `records 0-9/100` |
|
|
200
|
+
| Items 20-29 of 100 | `records 20-29/100` |
|
|
201
|
+
| No items found | `records */0` |
|
|
202
|
+
| Last page (items 90-99) | `records 90-99/100` |
|
|
203
|
+
|
|
204
|
+
### Performance Note
|
|
205
|
+
|
|
206
|
+
When `shouldQueryRange: true`, the repository executes the data query and count query **in parallel** using `Promise.all` for optimal performance.
|
|
207
|
+
|
|
208
|
+
|
|
143
209
|
## Combined Example
|
|
144
210
|
|
|
145
211
|
```typescript
|
|
@@ -153,3 +219,21 @@ await repo.find({
|
|
|
153
219
|
}
|
|
154
220
|
});
|
|
155
221
|
```
|
|
222
|
+
|
|
223
|
+
### With Range Information
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const { data, range } = await repo.find({
|
|
227
|
+
filter: {
|
|
228
|
+
where: { status: 'active' },
|
|
229
|
+
fields: ['id', 'name', 'price', 'createdAt'],
|
|
230
|
+
order: ['price ASC', 'createdAt DESC'],
|
|
231
|
+
limit: 20,
|
|
232
|
+
skip: 0
|
|
233
|
+
},
|
|
234
|
+
options: { shouldQueryRange: true }
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
console.log(`Showing ${range.start}-${range.end} of ${range.total}`);
|
|
238
|
+
// → "Showing 0-19 of 150"
|
|
239
|
+
```
|
|
@@ -51,9 +51,10 @@ The error handler middleware catches all unhandled errors in your application an
|
|
|
51
51
|
|
|
52
52
|
- **Automatic Error Formatting**: Converts all errors to structured JSON responses
|
|
53
53
|
- **ZodError Support**: Special handling for Zod validation errors with detailed field-level messages
|
|
54
|
+
- **Database Error Handling**: Automatically returns 400 for database constraint violations (unique, foreign key, not null, etc.)
|
|
54
55
|
- **Environment-Aware**: Hides stack traces and error causes in production
|
|
55
56
|
- **Request Tracking**: Includes `requestId` for debugging and tracing
|
|
56
|
-
- **Status Code Detection**: Automatically extracts
|
|
57
|
+
- **Status Code Detection**: Automatically extracts `statusCode` from errors
|
|
57
58
|
|
|
58
59
|
#### Usage
|
|
59
60
|
|
|
@@ -110,6 +111,37 @@ app.onError(appErrorHandler({
|
|
|
110
111
|
}
|
|
111
112
|
```
|
|
112
113
|
|
|
114
|
+
**Database Constraint Error:**
|
|
115
|
+
|
|
116
|
+
Database constraint violations (unique, foreign key, not null, check) are automatically detected and returned as 400 Bad Request with a human-readable message:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"message": "Unique constraint violation\nDetail: Key (email)=(test@example.com) already exists.\nTable: User\nConstraint: UQ_User_email",
|
|
121
|
+
"statusCode": 400,
|
|
122
|
+
"requestId": "abc123",
|
|
123
|
+
"details": {
|
|
124
|
+
"url": "http://localhost:3000/api/users",
|
|
125
|
+
"path": "/api/users",
|
|
126
|
+
"stack": "...", // development only
|
|
127
|
+
"cause": { ... } // development only
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Supported PostgreSQL Error Codes:**
|
|
133
|
+
|
|
134
|
+
| Code | Error Type |
|
|
135
|
+
|------|------------|
|
|
136
|
+
| 23505 | Unique constraint violation |
|
|
137
|
+
| 23503 | Foreign key constraint violation |
|
|
138
|
+
| 23502 | Not null constraint violation |
|
|
139
|
+
| 23514 | Check constraint violation |
|
|
140
|
+
| 23P01 | Exclusion constraint violation |
|
|
141
|
+
| 22P02 | Invalid text representation |
|
|
142
|
+
| 22003 | Numeric value out of range |
|
|
143
|
+
| 22001 | String data too long |
|
|
144
|
+
|
|
113
145
|
#### API Reference
|
|
114
146
|
|
|
115
147
|
##### `appErrorHandler(options)`
|
|
@@ -709,6 +709,7 @@ generateUserAuditColumnDefs(opts?: TUserAuditEnricherOptions): {
|
|
|
709
709
|
type TUserAuditColumnOpts = {
|
|
710
710
|
dataType: 'string' | 'number'; // Required - type of user ID
|
|
711
711
|
columnName: string; // Column name in database
|
|
712
|
+
allowAnonymous?: boolean; // Allow null user ID (default: true)
|
|
712
713
|
};
|
|
713
714
|
|
|
714
715
|
type TUserAuditEnricherOptions = {
|
|
@@ -718,8 +719,27 @@ type TUserAuditEnricherOptions = {
|
|
|
718
719
|
```
|
|
719
720
|
|
|
720
721
|
**Default values:**
|
|
721
|
-
- `created`: `{ dataType: 'number', columnName: 'created_by' }`
|
|
722
|
-
- `modified`: `{ dataType: 'number', columnName: 'modified_by' }`
|
|
722
|
+
- `created`: `{ dataType: 'number', columnName: 'created_by', allowAnonymous: true }`
|
|
723
|
+
- `modified`: `{ dataType: 'number', columnName: 'modified_by', allowAnonymous: true }`
|
|
724
|
+
|
|
725
|
+
#### `allowAnonymous` Behavior
|
|
726
|
+
|
|
727
|
+
The `allowAnonymous` option controls whether the enricher requires an authenticated user context:
|
|
728
|
+
|
|
729
|
+
| `allowAnonymous` | No Context | No User ID | Has User ID |
|
|
730
|
+
|------------------|------------|------------|-------------|
|
|
731
|
+
| `true` (default) | Returns `null` | Returns `null` | Returns user ID |
|
|
732
|
+
| `false` | Throws error | Throws error | Returns user ID |
|
|
733
|
+
|
|
734
|
+
**When to use `allowAnonymous: false`:**
|
|
735
|
+
- Sensitive audit trails that must track the responsible user
|
|
736
|
+
- Tables where anonymous operations should be forbidden
|
|
737
|
+
- Compliance requirements that mandate user attribution
|
|
738
|
+
|
|
739
|
+
**When to use `allowAnonymous: true` (default):**
|
|
740
|
+
- Background jobs, migrations, or seed scripts without user context
|
|
741
|
+
- System-generated records
|
|
742
|
+
- Tables that allow both authenticated and anonymous operations
|
|
723
743
|
|
|
724
744
|
#### Generated Columns
|
|
725
745
|
|
|
@@ -795,6 +815,24 @@ export const myTable = pgTable('MyTable', {
|
|
|
795
815
|
// modifiedBy: integer('editor_id')
|
|
796
816
|
```
|
|
797
817
|
|
|
818
|
+
**Requiring authenticated user (allowAnonymous: false):**
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
// For sensitive tables that must track the responsible user
|
|
822
|
+
export const auditLogTable = pgTable('AuditLog', {
|
|
823
|
+
...generateIdColumnDefs(),
|
|
824
|
+
...generateUserAuditColumnDefs({
|
|
825
|
+
created: { dataType: 'number', columnName: 'created_by', allowAnonymous: false },
|
|
826
|
+
modified: { dataType: 'number', columnName: 'modified_by', allowAnonymous: false },
|
|
827
|
+
}),
|
|
828
|
+
action: text('action').notNull(),
|
|
829
|
+
details: text('details'),
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// If no authenticated user context is available, throws:
|
|
833
|
+
// Error: [getCurrentUserId] Invalid request context to identify user | columnName: createdBy | allowAnonymous: false
|
|
834
|
+
```
|
|
835
|
+
|
|
798
836
|
|
|
799
837
|
## Schema Utilities
|
|
800
838
|
|
|
@@ -39,6 +39,7 @@ export class TodoRepository extends DefaultCRUDRepository<typeof Todo.schema> {
|
|
|
39
39
|
| Method | Description | Example |
|
|
40
40
|
|--------|-------------|---------|
|
|
41
41
|
| `find(opts)` | Find multiple records | `repo.find({ filter: { where: { status: 'active' } } })` |
|
|
42
|
+
| `find(opts)` with range | Find with pagination range | `repo.find({ filter, options: { shouldQueryRange: true } })` |
|
|
42
43
|
| `findOne(opts)` | Find single record | `repo.findOne({ filter: { where: { email } } })` |
|
|
43
44
|
| `findById(opts)` | Find by primary key | `repo.findById({ id: '123' })` |
|
|
44
45
|
| `count(opts)` | Count matching records | `repo.count({ where: { status: 'active' } })` |
|
|
@@ -186,6 +187,7 @@ await repo.deleteAll({ where: {}, options: { force: true } });
|
|
|
186
187
|
| Want to... | Code |
|
|
187
188
|
|------------|------|
|
|
188
189
|
| Find all active | `repo.find({ filter: { where: { status: 'active' } } })` |
|
|
190
|
+
| Find with range info | `repo.find({ filter, options: { shouldQueryRange: true } })` |
|
|
189
191
|
| Find by ID | `repo.findById({ id: '123' })` |
|
|
190
192
|
| Find with relations | `repo.find({ filter: { include: [{ relation: 'posts' }] } })` |
|
|
191
193
|
| Create one | `repo.create({ data: { name: 'John' } })` |
|