@venizia/ignis-docs 0.0.3 → 0.0.4-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 +1 -1
- package/package.json +4 -2
- package/wiki/best-practices/api-usage-examples.md +591 -0
- package/wiki/best-practices/architectural-patterns.md +415 -0
- package/wiki/best-practices/architecture-decisions.md +488 -0
- package/wiki/{get-started/best-practices → best-practices}/code-style-standards.md +406 -17
- package/wiki/{get-started/best-practices → best-practices}/common-pitfalls.md +109 -4
- package/wiki/{get-started/best-practices → best-practices}/contribution-workflow.md +34 -7
- package/wiki/best-practices/data-modeling.md +376 -0
- package/wiki/best-practices/deployment-strategies.md +698 -0
- package/wiki/best-practices/index.md +27 -0
- package/wiki/best-practices/performance-optimization.md +196 -0
- package/wiki/best-practices/security-guidelines.md +218 -0
- package/wiki/{get-started/best-practices → best-practices}/troubleshooting-tips.md +97 -1
- package/wiki/changelogs/2025-12-16-initial-architecture.md +1 -1
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +1 -1
- package/wiki/changelogs/2025-12-17-refactor.md +1 -1
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +5 -5
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +13 -7
- package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +2 -2
- package/wiki/changelogs/2025-12-29-dynamic-binding-registration.md +104 -0
- package/wiki/changelogs/2025-12-29-snowflake-uid-helper.md +100 -0
- package/wiki/changelogs/2025-12-30-repository-enhancements.md +214 -0
- package/wiki/changelogs/2025-12-31-json-path-filtering-array-operators.md +214 -0
- package/wiki/changelogs/2025-12-31-string-id-custom-generator.md +137 -0
- package/wiki/changelogs/2026-01-02-default-filter-and-repository-mixins.md +418 -0
- package/wiki/changelogs/index.md +6 -0
- package/wiki/changelogs/planned-schema-migrator.md +0 -8
- package/wiki/{get-started/core-concepts → guides/core-concepts/application}/bootstrapping.md +18 -5
- package/wiki/{get-started/core-concepts/application.md → guides/core-concepts/application/index.md} +47 -104
- package/wiki/guides/core-concepts/components-guide.md +509 -0
- package/wiki/{get-started → guides}/core-concepts/components.md +24 -17
- package/wiki/{get-started → guides}/core-concepts/controllers.md +30 -13
- package/wiki/{get-started → guides}/core-concepts/dependency-injection.md +97 -0
- package/wiki/guides/core-concepts/persistent/datasources.md +179 -0
- package/wiki/guides/core-concepts/persistent/index.md +119 -0
- package/wiki/guides/core-concepts/persistent/models.md +241 -0
- package/wiki/guides/core-concepts/persistent/repositories.md +219 -0
- package/wiki/guides/core-concepts/persistent/transactions.md +170 -0
- package/wiki/{get-started → guides}/core-concepts/services.md +26 -3
- package/wiki/{get-started → guides/get-started}/5-minute-quickstart.md +59 -14
- package/wiki/guides/get-started/philosophy.md +682 -0
- package/wiki/guides/get-started/setup.md +157 -0
- package/wiki/guides/index.md +89 -0
- package/wiki/guides/reference/glossary.md +243 -0
- package/wiki/{get-started → guides/reference}/mcp-docs-server.md +0 -10
- package/wiki/{get-started → guides/tutorials}/building-a-crud-api.md +134 -132
- package/wiki/{get-started/quickstart.md → guides/tutorials/complete-installation.md} +107 -71
- package/wiki/guides/tutorials/ecommerce-api.md +1399 -0
- package/wiki/guides/tutorials/realtime-chat.md +1261 -0
- package/wiki/guides/tutorials/testing.md +723 -0
- package/wiki/index.md +176 -37
- package/wiki/references/base/application.md +27 -0
- package/wiki/references/base/bootstrapping.md +30 -26
- package/wiki/references/base/components.md +24 -7
- package/wiki/references/base/controllers.md +51 -20
- package/wiki/references/base/datasources.md +30 -0
- package/wiki/references/base/dependency-injection.md +39 -3
- package/wiki/references/base/filter-system/application-usage.md +224 -0
- package/wiki/references/base/filter-system/array-operators.md +132 -0
- package/wiki/references/base/filter-system/comparison-operators.md +109 -0
- package/wiki/references/base/filter-system/default-filter.md +428 -0
- package/wiki/references/base/filter-system/fields-order-pagination.md +155 -0
- package/wiki/references/base/filter-system/index.md +127 -0
- package/wiki/references/base/filter-system/json-filtering.md +197 -0
- package/wiki/references/base/filter-system/list-operators.md +71 -0
- package/wiki/references/base/filter-system/logical-operators.md +156 -0
- package/wiki/references/base/filter-system/null-operators.md +58 -0
- package/wiki/references/base/filter-system/pattern-matching.md +108 -0
- package/wiki/references/base/filter-system/quick-reference.md +431 -0
- package/wiki/references/base/filter-system/range-operators.md +63 -0
- package/wiki/references/base/filter-system/tips.md +190 -0
- package/wiki/references/base/filter-system/use-cases.md +452 -0
- package/wiki/references/base/index.md +90 -0
- package/wiki/references/base/middlewares.md +602 -0
- package/wiki/references/base/models.md +215 -23
- package/wiki/references/base/providers.md +732 -0
- package/wiki/references/base/repositories/advanced.md +555 -0
- package/wiki/references/base/repositories/index.md +228 -0
- package/wiki/references/base/repositories/mixins.md +331 -0
- package/wiki/references/base/repositories/relations.md +486 -0
- package/wiki/references/base/repositories.md +40 -635
- package/wiki/references/base/services.md +28 -4
- package/wiki/references/components/authentication.md +22 -2
- package/wiki/references/components/health-check.md +12 -0
- package/wiki/references/components/index.md +23 -0
- package/wiki/references/components/mail.md +687 -0
- package/wiki/references/components/request-tracker.md +16 -0
- package/wiki/references/components/socket-io.md +18 -0
- package/wiki/references/components/static-asset.md +14 -26
- package/wiki/references/components/swagger.md +17 -0
- package/wiki/references/configuration/environment-variables.md +427 -0
- package/wiki/references/configuration/index.md +73 -0
- package/wiki/references/helpers/cron.md +14 -0
- package/wiki/references/helpers/crypto.md +15 -0
- package/wiki/references/helpers/env.md +16 -0
- package/wiki/references/helpers/error.md +17 -0
- package/wiki/references/helpers/index.md +14 -0
- package/wiki/references/helpers/inversion.md +24 -4
- package/wiki/references/helpers/logger.md +19 -0
- package/wiki/references/helpers/network.md +11 -0
- package/wiki/references/helpers/queue.md +19 -0
- package/wiki/references/helpers/redis.md +21 -0
- package/wiki/references/helpers/socket-io.md +24 -5
- package/wiki/references/helpers/storage.md +18 -10
- package/wiki/references/helpers/testing.md +18 -0
- package/wiki/references/helpers/types.md +16 -0
- package/wiki/references/helpers/uid.md +167 -0
- package/wiki/references/helpers/worker-thread.md +16 -0
- package/wiki/references/index.md +177 -0
- package/wiki/references/quick-reference.md +634 -0
- package/wiki/references/src-details/boot.md +3 -3
- package/wiki/references/src-details/dev-configs.md +0 -4
- package/wiki/references/src-details/docs.md +2 -2
- package/wiki/references/src-details/index.md +86 -0
- package/wiki/references/src-details/inversion.md +1 -6
- package/wiki/references/src-details/mcp-server.md +3 -15
- package/wiki/references/utilities/index.md +86 -10
- package/wiki/references/utilities/jsx.md +577 -0
- package/wiki/references/utilities/request.md +0 -2
- package/wiki/references/utilities/statuses.md +740 -0
- package/wiki/get-started/best-practices/api-usage-examples.md +0 -266
- package/wiki/get-started/best-practices/architectural-patterns.md +0 -170
- package/wiki/get-started/best-practices/data-modeling.md +0 -177
- package/wiki/get-started/best-practices/deployment-strategies.md +0 -121
- package/wiki/get-started/best-practices/performance-optimization.md +0 -97
- package/wiki/get-started/best-practices/security-guidelines.md +0 -99
- package/wiki/get-started/core-concepts/persistent.md +0 -539
- package/wiki/get-started/index.md +0 -65
- package/wiki/get-started/philosophy.md +0 -296
- package/wiki/get-started/prerequisites.md +0 -113
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Performance Optimization
|
|
2
|
+
|
|
3
|
+
Optimize your Ignis application for speed and scalability.
|
|
4
|
+
|
|
5
|
+
## 1. Measure Performance
|
|
6
|
+
|
|
7
|
+
Identify bottlenecks before optimizing:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { executeWithPerformanceMeasure } from '@venizia/ignis';
|
|
11
|
+
|
|
12
|
+
await executeWithPerformanceMeasure({
|
|
13
|
+
logger: this.logger,
|
|
14
|
+
scope: 'DataProcessing',
|
|
15
|
+
description: 'Process large dataset',
|
|
16
|
+
task: async () => {
|
|
17
|
+
await processLargeDataset();
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Logs execution time automatically.
|
|
23
|
+
|
|
24
|
+
> **Deep Dive:** See [Performance Utility](../references/utilities/performance.md) for advanced profiling.
|
|
25
|
+
|
|
26
|
+
## 2. Offload CPU-Intensive Tasks
|
|
27
|
+
|
|
28
|
+
Prevent blocking the event loop with Worker Threads:
|
|
29
|
+
|
|
30
|
+
**Use Worker Threads for:**
|
|
31
|
+
- Complex calculations or crypto operations
|
|
32
|
+
- Large file/data processing
|
|
33
|
+
- Any synchronous task > 5ms
|
|
34
|
+
|
|
35
|
+
> **Deep Dive:** See [Worker Thread Helper](../references/helpers/worker-thread.md) for implementation guide.
|
|
36
|
+
|
|
37
|
+
## 3. Optimize Database Queries
|
|
38
|
+
|
|
39
|
+
| Technique | Example | Impact |
|
|
40
|
+
|-----------|---------|--------|
|
|
41
|
+
| **Select specific fields** | `fields: { id: true, name: true }` | Reduce data transfer |
|
|
42
|
+
| **Use indexes** | Create indexes on WHERE/JOIN columns | 10-100x faster queries |
|
|
43
|
+
| **Mandatory Limit** | `limit: 20` | Prevent fetching massive datasets |
|
|
44
|
+
| **Paginate results** | `limit: 20, offset: 0` | Prevent memory overflow |
|
|
45
|
+
| **Eager load relations** | `include: [{ relation: 'creator' }]` | Solve N+1 problem |
|
|
46
|
+
|
|
47
|
+
### Query Operators Reference
|
|
48
|
+
|
|
49
|
+
Ignis supports extensive query operators for filtering:
|
|
50
|
+
|
|
51
|
+
| Operator | Description | Example |
|
|
52
|
+
|----------|-------------|---------|
|
|
53
|
+
| `eq` | Equal (handles null) | `{ status: { eq: 'ACTIVE' } }` |
|
|
54
|
+
| `ne`, `neq` | Not equal | `{ status: { ne: 'DELETED' } }` |
|
|
55
|
+
| `gt`, `gte` | Greater than (or equal) | `{ age: { gte: 18 } }` |
|
|
56
|
+
| `lt`, `lte` | Less than (or equal) | `{ price: { lt: 100 } }` |
|
|
57
|
+
| `like` | SQL LIKE (case-sensitive) | `{ name: { like: '%john%' } }` |
|
|
58
|
+
| `ilike` | Case-insensitive LIKE | `{ email: { ilike: '%@gmail%' } }` |
|
|
59
|
+
| `nlike`, `nilike` | NOT LIKE variants | `{ name: { nlike: '%test%' } }` |
|
|
60
|
+
| `regexp` | PostgreSQL regex (`~`) | `{ code: { regexp: '^[A-Z]+$' } }` |
|
|
61
|
+
| `iregexp` | Case-insensitive regex (`~*`) | `{ name: { iregexp: '^john' } }` |
|
|
62
|
+
| `in`, `inq` | Value in array | `{ status: { in: ['A', 'B'] } }` |
|
|
63
|
+
| `nin` | Value NOT in array | `{ role: { nin: ['guest'] } }` |
|
|
64
|
+
| `between` | Range (inclusive) | `{ age: { between: [18, 65] } }` |
|
|
65
|
+
| `is`, `isn` | IS NULL / IS NOT NULL | `{ deletedAt: { is: null } }` |
|
|
66
|
+
| `and`, `or` | Logical operators | `{ or: [{ a: 1 }, { b: 2 }] }` |
|
|
67
|
+
|
|
68
|
+
**Complex Filter Example:**
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
await repo.find({
|
|
72
|
+
filter: {
|
|
73
|
+
where: {
|
|
74
|
+
and: [
|
|
75
|
+
{ status: { in: ['ACTIVE', 'PENDING'] } },
|
|
76
|
+
{ createdAt: { gte: new Date('2024-01-01') } },
|
|
77
|
+
{ or: [
|
|
78
|
+
{ role: 'admin' },
|
|
79
|
+
{ permissions: { ilike: '%manage%' } }
|
|
80
|
+
]}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
limit: 50,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### JSON Path Filtering
|
|
89
|
+
|
|
90
|
+
Filter by nested JSON/JSONB fields using PostgreSQL's `#>` operator:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Order by nested JSON path
|
|
94
|
+
await repo.find({
|
|
95
|
+
filter: {
|
|
96
|
+
order: ['metadata.nested[0].field ASC'],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// The framework uses PostgreSQL #> operator for path extraction
|
|
101
|
+
// metadata #> '{nested,0,field}'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> [!TIP]
|
|
105
|
+
> **Avoid Deep Nesting:** While Ignis supports deeply nested `include` filters, each level adds significant overhead to query construction and result mapping. We strongly recommend a **maximum of 2 levels** (e.g., `User -> Orders -> Items`). For more complex data fetching, consider separate queries.
|
|
106
|
+
|
|
107
|
+
**Example:**
|
|
108
|
+
```typescript
|
|
109
|
+
await userRepository.find({
|
|
110
|
+
filter: {
|
|
111
|
+
fields: { id: true, name: true, email: true }, // ✅ Specific fields
|
|
112
|
+
where: { status: 'ACTIVE' },
|
|
113
|
+
limit: 20, // ✅ Mandatory limit
|
|
114
|
+
include: [{
|
|
115
|
+
relation: 'orders',
|
|
116
|
+
scope: {
|
|
117
|
+
include: [{ relation: 'items' }] // ✅ Level 2 (Recommended limit)
|
|
118
|
+
}
|
|
119
|
+
}],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 4. Implement Caching
|
|
125
|
+
|
|
126
|
+
Reduce database load with caching:
|
|
127
|
+
|
|
128
|
+
| Cache Type | Use Case | Implementation |
|
|
129
|
+
|-----------|----------|----------------|
|
|
130
|
+
| **Redis** | Distributed cache, session storage | [Redis Helper](../references/helpers/redis.md) |
|
|
131
|
+
| **In-Memory** | Single-process cache | `MemoryStorageHelper` |
|
|
132
|
+
|
|
133
|
+
**Example:**
|
|
134
|
+
```typescript
|
|
135
|
+
// Cache expensive query results
|
|
136
|
+
const cached = await redis.get('users:active');
|
|
137
|
+
if (!cached) {
|
|
138
|
+
const users = await userRepository.find({ where: { active: true } });
|
|
139
|
+
await redis.set('users:active', users, 300); // 5 min TTL
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 5. Production Settings
|
|
144
|
+
|
|
145
|
+
| Setting | Value | Why |
|
|
146
|
+
|---------|-------|-----|
|
|
147
|
+
| `NODE_ENV` | `production` | Enables library optimizations |
|
|
148
|
+
| Process Manager | PM2, systemd, Docker | Auto-restart, cluster mode |
|
|
149
|
+
| Cluster Mode | CPU cores | Utilize all CPUs |
|
|
150
|
+
|
|
151
|
+
**PM2 Cluster Mode:**
|
|
152
|
+
```bash
|
|
153
|
+
pm2 start dist/index.js -i max # Use all CPU cores
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 6. Transaction Support
|
|
157
|
+
|
|
158
|
+
Use transactions to ensure atomicity across multiple database operations:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// Start a transaction
|
|
162
|
+
const tx = await userRepository.beginTransaction({
|
|
163
|
+
isolationLevel: 'READ COMMITTED', // or 'REPEATABLE READ' | 'SERIALIZABLE'
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// Pass transaction to all operations
|
|
168
|
+
const user = await userRepository.create({
|
|
169
|
+
data: { name: 'John', email: 'john@example.com' },
|
|
170
|
+
options: { transaction: tx },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await orderRepository.create({
|
|
174
|
+
data: { userId: user.data.id, total: 100 },
|
|
175
|
+
options: { transaction: tx },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Commit if all operations succeed
|
|
179
|
+
await tx.commit();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Rollback on any failure
|
|
182
|
+
await tx.rollback();
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Transaction Isolation Levels:**
|
|
188
|
+
|
|
189
|
+
| Level | Description | Use Case |
|
|
190
|
+
|-------|-------------|----------|
|
|
191
|
+
| `READ COMMITTED` | See committed data only (default) | Most CRUD operations |
|
|
192
|
+
| `REPEATABLE READ` | Consistent reads within transaction | Reports, aggregations |
|
|
193
|
+
| `SERIALIZABLE` | Full isolation, may retry | Financial transactions |
|
|
194
|
+
|
|
195
|
+
> [!WARNING]
|
|
196
|
+
> Higher isolation levels reduce concurrency. Use `READ COMMITTED` unless you have specific consistency requirements.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Security Guidelines
|
|
2
|
+
|
|
3
|
+
Critical security practices to protect your Ignis application.
|
|
4
|
+
|
|
5
|
+
## 1. Secret Management
|
|
6
|
+
|
|
7
|
+
**Never hard-code secrets.** Use environment variables for all sensitive data.
|
|
8
|
+
|
|
9
|
+
| Environment | Where to Store Secrets |
|
|
10
|
+
|-------------|----------------------|
|
|
11
|
+
| Development | `.env` file (add to `.gitignore`) |
|
|
12
|
+
| Production | Cloud provider's secret manager (AWS Secrets Manager, Azure Key Vault, etc.) |
|
|
13
|
+
|
|
14
|
+
**Example `.env`:**
|
|
15
|
+
```bash
|
|
16
|
+
APP_ENV_APPLICATION_SECRET=your_strong_random_secret_here
|
|
17
|
+
APP_ENV_JWT_SECRET=another_strong_random_secret_here
|
|
18
|
+
APP_ENV_POSTGRES_PASSWORD=database_password_here
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Generate strong secrets:**
|
|
22
|
+
```bash
|
|
23
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 2. Input Validation
|
|
27
|
+
|
|
28
|
+
**Always validate incoming data** with Zod schemas. Ignis automatically rejects invalid requests.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { z } from '@hono/zod-openapi';
|
|
32
|
+
import { jsonContent, jsonResponse } from '@venizia/ignis';
|
|
33
|
+
|
|
34
|
+
const CreateUserRoute = {
|
|
35
|
+
method: 'post',
|
|
36
|
+
path: '/users',
|
|
37
|
+
request: {
|
|
38
|
+
body: jsonContent({
|
|
39
|
+
schema: z.object({
|
|
40
|
+
email: z.string().email(), // Valid email
|
|
41
|
+
age: z.number().int().min(18), // Adult only
|
|
42
|
+
role: z.enum(['user', 'admin']), // Whitelist
|
|
43
|
+
}),
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
responses: jsonResponse({ /* ... */ }),
|
|
47
|
+
} as const;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Validation happens automatically** - invalid requests never reach your handler.
|
|
51
|
+
|
|
52
|
+
## 3. Authentication & Authorization
|
|
53
|
+
|
|
54
|
+
Protect sensitive endpoints with `AuthenticateComponent`.
|
|
55
|
+
|
|
56
|
+
**Setup:**
|
|
57
|
+
```typescript
|
|
58
|
+
// application.ts
|
|
59
|
+
this.component(AuthenticateComponent);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Protect routes:**
|
|
63
|
+
```typescript
|
|
64
|
+
const SecureRoute = {
|
|
65
|
+
path: '/admin/users',
|
|
66
|
+
authStrategies: [Authentication.STRATEGY_JWT], // Requires JWT
|
|
67
|
+
// ...
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> **Deep Dive:** See [Authentication Component](../references/components/authentication.md) for full setup guide.
|
|
72
|
+
|
|
73
|
+
**Access user in protected routes:**
|
|
74
|
+
```typescript
|
|
75
|
+
import { Authentication, IJWTTokenPayload, ApplicationError, getError } from '@venizia/ignis';
|
|
76
|
+
|
|
77
|
+
const user = c.get(Authentication.CURRENT_USER) as IJWTTokenPayload;
|
|
78
|
+
if (!user.roles.includes('admin')) {
|
|
79
|
+
throw getError({ statusCode: 403, message: 'Forbidden' });
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 4. Protecting Sensitive Data with Hidden Properties
|
|
84
|
+
|
|
85
|
+
Configure model properties that should **never be returned** through repository queries. Hidden properties are excluded at the SQL level - they never leave the database.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
@model({
|
|
89
|
+
type: 'entity',
|
|
90
|
+
settings: {
|
|
91
|
+
hiddenProperties: ['password', 'apiSecret', 'internalToken'],
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
95
|
+
static override schema = pgTable('User', {
|
|
96
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
97
|
+
email: text('email').notNull(),
|
|
98
|
+
password: text('password'), // Never returned via repository
|
|
99
|
+
apiSecret: text('api_secret'), // Never returned via repository
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Why SQL-level exclusion matters:**
|
|
105
|
+
|
|
106
|
+
| Approach | Security Level | Data Exposure Risk |
|
|
107
|
+
|----------|---------------|-------------------|
|
|
108
|
+
| Post-query filtering (JS) | Low | Data passes through network/memory |
|
|
109
|
+
| **SQL-level exclusion** | **High** | **Data never leaves database** |
|
|
110
|
+
|
|
111
|
+
**When you legitimately need hidden data** (e.g., password verification), use the connector directly:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// For authentication - access password via connector
|
|
115
|
+
const connector = userRepo.getConnector();
|
|
116
|
+
const [user] = await connector
|
|
117
|
+
.select({ id: User.schema.id, password: User.schema.password })
|
|
118
|
+
.from(User.schema)
|
|
119
|
+
.where(eq(User.schema.email, email));
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> **Reference:** See [Hidden Properties](../references/base/models.md#hidden-properties) for complete documentation.
|
|
123
|
+
|
|
124
|
+
## 5. File Upload Security
|
|
125
|
+
|
|
126
|
+
When handling file uploads, prevent **path traversal attacks** and ensure safe file handling.
|
|
127
|
+
|
|
128
|
+
### Path Traversal Prevention
|
|
129
|
+
|
|
130
|
+
**Problem:** Malicious filenames like `../../../etc/passwd` can write files outside intended directories.
|
|
131
|
+
|
|
132
|
+
**Solution:** Use `sanitizeFilename()` to strip dangerous patterns:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { sanitizeFilename } from '@venizia/ignis';
|
|
136
|
+
|
|
137
|
+
// ❌ DANGEROUS - User-controlled filename
|
|
138
|
+
const unsafeFilename = req.body.filename; // Could be "../../../etc/passwd"
|
|
139
|
+
fs.writeFileSync(`./uploads/${unsafeFilename}`, data);
|
|
140
|
+
|
|
141
|
+
// ✅ SAFE - Sanitized filename
|
|
142
|
+
const safeFilename = sanitizeFilename(req.body.filename);
|
|
143
|
+
fs.writeFileSync(`./uploads/${safeFilename}`, data);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**What `sanitizeFilename()` does:**
|
|
147
|
+
- Extracts basename (removes directory paths)
|
|
148
|
+
- Removes dangerous characters (`../`, special chars)
|
|
149
|
+
- Replaces consecutive dots with single dot
|
|
150
|
+
- Returns `'download'` for empty/suspicious patterns
|
|
151
|
+
|
|
152
|
+
### Safe File Download Headers
|
|
153
|
+
|
|
154
|
+
Use `createContentDispositionHeader()` for secure download responses:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { createContentDispositionHeader, sanitizeFilename } from '@venizia/ignis';
|
|
158
|
+
|
|
159
|
+
async downloadFile(c: Context) {
|
|
160
|
+
const filename = sanitizeFilename(c.req.param('filename'));
|
|
161
|
+
const fileBuffer = await fs.readFile(`./uploads/${filename}`);
|
|
162
|
+
|
|
163
|
+
return new Response(fileBuffer, {
|
|
164
|
+
headers: {
|
|
165
|
+
'Content-Type': 'application/octet-stream',
|
|
166
|
+
'Content-Disposition': createContentDispositionHeader({
|
|
167
|
+
filename: filename,
|
|
168
|
+
type: 'attachment',
|
|
169
|
+
}),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Built-in Multipart Parsing
|
|
176
|
+
|
|
177
|
+
Use `parseMultipartBody()` for safe file uploads with automatic sanitization:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { parseMultipartBody } from '@venizia/ignis';
|
|
181
|
+
|
|
182
|
+
async uploadFile(c: Context) {
|
|
183
|
+
const files = await parseMultipartBody({
|
|
184
|
+
context: c,
|
|
185
|
+
storage: 'disk', // or 'memory' for buffer
|
|
186
|
+
uploadDir: './uploads', // Target directory
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Files are saved with sanitized names: timestamp-random-sanitized_name.ext
|
|
190
|
+
return c.json({ uploaded: files.map(f => f.filename) });
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Security features:**
|
|
195
|
+
- Automatic filename sanitization
|
|
196
|
+
- Creates upload directory if missing
|
|
197
|
+
- Generates unique filenames (prevents overwrites)
|
|
198
|
+
- Returns file metadata (size, mimetype) for validation
|
|
199
|
+
|
|
200
|
+
> **Reference:** See [Request Utility](../references/utilities/request.md) for full API documentation.
|
|
201
|
+
|
|
202
|
+
## 6. Secure Dependencies
|
|
203
|
+
|
|
204
|
+
Regularly audit and update dependencies:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Check for vulnerabilities
|
|
208
|
+
bun audit
|
|
209
|
+
|
|
210
|
+
# Update dependencies
|
|
211
|
+
bun update
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Critical packages to keep updated:**
|
|
215
|
+
- `hono` - Web framework
|
|
216
|
+
- `jose` - JWT handling
|
|
217
|
+
- `drizzle-orm` - Database ORM
|
|
218
|
+
- `@venizia/ignis` - Framework core
|
|
@@ -97,4 +97,100 @@ cat .env | grep APP_ENV
|
|
|
97
97
|
- Use `try-catch` blocks to catch and log errors
|
|
98
98
|
- Check database queries with Drizzle's logging: `{ logger: true }`
|
|
99
99
|
|
|
100
|
-
> **Deep Dive:** See [Logger Helper](
|
|
100
|
+
> **Deep Dive:** See [Logger Helper](../references/helpers/logger.md) for advanced logging configuration.
|
|
101
|
+
|
|
102
|
+
## 6. Request ID Tracking
|
|
103
|
+
|
|
104
|
+
Every request in Ignis is automatically assigned a unique `requestId` for log correlation. The `RequestSpyMiddleware` logs this ID at the start and end of each request.
|
|
105
|
+
|
|
106
|
+
**Log output format:**
|
|
107
|
+
```
|
|
108
|
+
[spy][abc123] START | Handling Request | forwardedIp: 192.168.1.1 | path: /api/users | method: GET
|
|
109
|
+
[spy][abc123] DONE | Handling Request | forwardedIp: 192.168.1.1 | path: /api/users | method: GET | Took: 45.2 (ms)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Access request ID in handlers:**
|
|
113
|
+
```typescript
|
|
114
|
+
import { RequestSpyMiddleware } from '@venizia/ignis';
|
|
115
|
+
|
|
116
|
+
// Inside a controller method
|
|
117
|
+
async getUser(c: Context) {
|
|
118
|
+
const requestId = c.get(RequestSpyMiddleware.REQUEST_ID_KEY);
|
|
119
|
+
this.logger.info('[%s] Processing user request', requestId);
|
|
120
|
+
// ...
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Filtering logs by request:**
|
|
125
|
+
```bash
|
|
126
|
+
# Find all logs for a specific request
|
|
127
|
+
grep "abc123" logs/app.log
|
|
128
|
+
|
|
129
|
+
# Extract request timing
|
|
130
|
+
grep "\[spy\]\[abc123\]" logs/app.log
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Why this matters:**
|
|
134
|
+
- Correlate logs across services in distributed systems
|
|
135
|
+
- Debug specific user issues by their request ID
|
|
136
|
+
- Measure request duration from START to DONE timestamps
|
|
137
|
+
|
|
138
|
+
## 7. Validation Error Debugging
|
|
139
|
+
|
|
140
|
+
When Zod validation fails, Ignis returns a structured error response. Understanding this format helps debug client-side issues.
|
|
141
|
+
|
|
142
|
+
**Error response structure:**
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"statusCode": 422,
|
|
146
|
+
"message": "ValidationError",
|
|
147
|
+
"requestId": "abc123",
|
|
148
|
+
"details": {
|
|
149
|
+
"cause": [
|
|
150
|
+
{
|
|
151
|
+
"path": "email",
|
|
152
|
+
"message": "Invalid email",
|
|
153
|
+
"code": "invalid_string",
|
|
154
|
+
"expected": "email",
|
|
155
|
+
"received": "string"
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Common validation error codes:**
|
|
163
|
+
|
|
164
|
+
| Code | Meaning | Example |
|
|
165
|
+
|------|---------|---------|
|
|
166
|
+
| `invalid_type` | Wrong data type | Expected `number`, got `string` |
|
|
167
|
+
| `invalid_string` | String format invalid | Invalid email or UUID format |
|
|
168
|
+
| `too_small` | Value below minimum | String shorter than min length |
|
|
169
|
+
| `too_big` | Value above maximum | Number exceeds max value |
|
|
170
|
+
| `invalid_enum_value` | Value not in enum | Status must be 'ACTIVE' or 'INACTIVE' |
|
|
171
|
+
| `unrecognized_keys` | Extra fields in request | Strict schema rejects unknown fields |
|
|
172
|
+
|
|
173
|
+
**Debugging tips:**
|
|
174
|
+
|
|
175
|
+
1. **Check the `path` field** - Shows which field failed validation
|
|
176
|
+
2. **Compare `expected` vs `received`** - Identifies type mismatches
|
|
177
|
+
3. **Review schema definition** - Ensure client sends correct format
|
|
178
|
+
|
|
179
|
+
**Example: Debugging nested validation errors:**
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"details": {
|
|
183
|
+
"cause": [
|
|
184
|
+
{
|
|
185
|
+
"path": "address.zipCode",
|
|
186
|
+
"message": "Expected string, received number",
|
|
187
|
+
"code": "invalid_type",
|
|
188
|
+
"expected": "string",
|
|
189
|
+
"received": "number"
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The `path` uses dot notation for nested objects. Here, `address.zipCode` means the `zipCode` field inside the `address` object is invalid.
|
|
@@ -11,7 +11,7 @@ This update focuses on performance improvements for the repository layer, reduci
|
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
14
|
-
- **WeakMap Cache**: `
|
|
14
|
+
- **WeakMap Cache**: `FilterBuilder` now caches `getTableColumns()` results.
|
|
15
15
|
- **Core API for Flat Queries**: `ReadableRepository` uses faster Drizzle Core API when possible.
|
|
16
16
|
- **Static schemaFactory Singleton**: `BaseEntity` shares a single `schemaFactory` instance across all entities.
|
|
17
17
|
- **Async/Await Refactor**: Removed redundant Promise wrappers from repository methods.
|
|
@@ -27,7 +27,7 @@ This update focuses on performance improvements for the repository layer, reduci
|
|
|
27
27
|
**Solution:** Added static WeakMap cache that stores column metadata per schema.
|
|
28
28
|
|
|
29
29
|
```typescript
|
|
30
|
-
export class
|
|
30
|
+
export class FilterBuilder extends BaseHelper {
|
|
31
31
|
// Static cache shared across all instances
|
|
32
32
|
private static columnCache = new WeakMap<
|
|
33
33
|
TTableSchemaWithId,
|
|
@@ -35,10 +35,10 @@ export class DrizzleFilterBuilder extends BaseHelper {
|
|
|
35
35
|
>();
|
|
36
36
|
|
|
37
37
|
private getColumns<Schema extends TTableSchemaWithId>(schema: Schema) {
|
|
38
|
-
let columns =
|
|
38
|
+
let columns = FilterBuilder.columnCache.get(schema);
|
|
39
39
|
if (!columns) {
|
|
40
40
|
columns = getTableColumns(schema);
|
|
41
|
-
|
|
41
|
+
FilterBuilder.columnCache.set(schema, columns);
|
|
42
42
|
}
|
|
43
43
|
return columns;
|
|
44
44
|
}
|
|
@@ -127,4 +127,4 @@ return { count };
|
|
|
127
127
|
|
|
128
128
|
## No Breaking Changes
|
|
129
129
|
|
|
130
|
-
All changes are internal optimizations. No API changes or migration required.
|
|
130
|
+
All changes are internal optimizations. No API changes or migration required.
|
|
@@ -16,7 +16,7 @@ This update adds strict validation to the `@repository` decorator and fixes seve
|
|
|
16
16
|
- **DataSource Auto-Discovery**: Schema is automatically built from `@repository` bindings.
|
|
17
17
|
- **Filter Security**: Fixed empty IN array bypass, invalid column handling, BETWEEN validation.
|
|
18
18
|
- **PostgreSQL Compatibility**: REGEXP now uses PostgreSQL POSIX operators.
|
|
19
|
-
- **
|
|
19
|
+
- **String ID Generation**: Uses `text` column with customizable ID generator (default: `crypto.randomUUID()`).
|
|
20
20
|
|
|
21
21
|
## Breaking Changes
|
|
22
22
|
|
|
@@ -114,18 +114,24 @@ export class PostgresDataSource extends BaseDataSource<...> {
|
|
|
114
114
|
}
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
###
|
|
117
|
+
### String ID with Custom Generator
|
|
118
118
|
|
|
119
119
|
**File:** `packages/core/src/base/models/enrichers/id.enricher.ts`
|
|
120
120
|
|
|
121
|
-
**Problem:**
|
|
121
|
+
**Problem:** Need flexible ID generation with maximum database compatibility.
|
|
122
122
|
|
|
123
|
-
**Solution:**
|
|
123
|
+
**Solution:** Uses `text` column with customizable ID generator (default: `crypto.randomUUID()`).
|
|
124
124
|
|
|
125
125
|
```typescript
|
|
126
|
-
//
|
|
126
|
+
// Default: text('id').primaryKey().$defaultFn(() => crypto.randomUUID())
|
|
127
|
+
// Custom: text('id').primaryKey().$defaultFn(() => nanoid())
|
|
127
128
|
```
|
|
128
129
|
|
|
130
|
+
**Benefits:**
|
|
131
|
+
- Maximum database compatibility with `text` column type
|
|
132
|
+
- Customizable ID generation (UUID, nanoid, cuid, etc.)
|
|
133
|
+
- Application-level ID generation via `$defaultFn()`
|
|
134
|
+
|
|
129
135
|
### Case-Insensitive REGEXP (IREGEXP)
|
|
130
136
|
|
|
131
137
|
**File:** `packages/core/src/base/repositories/operators/query.ts`
|
|
@@ -188,7 +194,7 @@ await repo.find({ filter: { where: { age: { BETWEEN: [10] } } } });
|
|
|
188
194
|
|------|---------|
|
|
189
195
|
| `src/base/models/base.ts` | Static schema/relations support, IEntity interface |
|
|
190
196
|
| `src/base/models/common/types.ts` | IEntity interface definition |
|
|
191
|
-
| `src/base/models/enrichers/id.enricher.ts` |
|
|
197
|
+
| `src/base/models/enrichers/id.enricher.ts` | Text column with customizable ID generator |
|
|
192
198
|
| `src/base/repositories/core/base.ts` | Constructor signature change, relations auto-resolution |
|
|
193
199
|
| `src/base/repositories/core/readable.ts` | Constructor change, getQueryInterface validation |
|
|
194
200
|
| `src/base/repositories/core/persistable.ts` | Constructor change, log option, TypeScript overloads |
|
|
@@ -246,4 +252,4 @@ Replace MySQL-style REGEXP with PostgreSQL syntax:
|
|
|
246
252
|
- `REGEXP` → uses `~` (case-sensitive)
|
|
247
253
|
- `IREGEXP` → uses `~*` (case-insensitive)
|
|
248
254
|
|
|
249
|
-
```
|
|
255
|
+
```
|
|
@@ -13,7 +13,7 @@ This update introduces support for deeply nested relation queries in repositorie
|
|
|
13
13
|
|
|
14
14
|
- **Nested Inclusions**: `include` filters now work recursively to any depth.
|
|
15
15
|
- **Generic Repository Methods**: `find<R>`, `findOne<R>`, etc. now support custom return types.
|
|
16
|
-
- **
|
|
16
|
+
- **FilterBuilder Decoupling**: Decoupled filter builder from MetadataRegistry for cleaner architecture.
|
|
17
17
|
|
|
18
18
|
## New Features
|
|
19
19
|
|
|
@@ -21,7 +21,7 @@ This update introduces support for deeply nested relation queries in repositorie
|
|
|
21
21
|
|
|
22
22
|
**File:** `packages/core/src/base/repositories/operators/filter.ts`
|
|
23
23
|
|
|
24
|
-
**Problem:** Previously, the `
|
|
24
|
+
**Problem:** Previously, the `FilterBuilder` could only resolve relations for the root entity. Nested includes (e.g., `include: [{ relation: 'a', scope: { include: [{ relation: 'b' }] } }]`) failed because it didn't know the schema of relation 'a'.
|
|
25
25
|
|
|
26
26
|
**Solution:** The builder now accepts a `relationResolver` function (injected from the Repository) which allows it to dynamically lookup schemas and relations for any entity during recursive traversal.
|
|
27
27
|
|