@venizia/ignis-docs 0.0.2 → 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 +647 -182
- 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 +86 -0
- package/wiki/changelogs/2025-12-26-transaction-support.md +57 -0
- 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 +8 -1
- package/wiki/changelogs/planned-schema-migrator.md +2 -10
- 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/guides/core-concepts/components.md +122 -0
- 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 +532 -31
- package/wiki/references/base/controllers.md +136 -38
- package/wiki/references/base/datasources.md +108 -5
- 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 -549
- 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 +15 -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 +167 -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/changelogs/planned-transaction-support.md +0 -216
- 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 -88
- package/wiki/get-started/best-practices/security-guidelines.md +0 -99
- package/wiki/get-started/core-concepts/components.md +0 -98
- package/wiki/get-started/core-concepts/persistent.md +0 -543
- package/wiki/get-started/index.md +0 -65
- package/wiki/get-started/philosophy.md +0 -296
- package/wiki/get-started/prerequisites.md +0 -113
|
@@ -125,14 +125,119 @@ APP_ENV_POSTGRES_DATABASE=db
|
|
|
125
125
|
|
|
126
126
|
## 5. Not Using `as const` for Route Definitions
|
|
127
127
|
|
|
128
|
-
**Pitfall:** When using the decorator-based routing with a shared `
|
|
128
|
+
**Pitfall:** When using the decorator-based routing with a shared `RouteConfigs` object, you forget to add `as const` to the object definition. TypeScript will infer the types too broadly, and you will lose the benefits of type-safe contexts (`TRouteContext`).
|
|
129
129
|
|
|
130
130
|
**Solution:** Always use `as const` when exporting a shared route configuration object.
|
|
131
131
|
|
|
132
132
|
**Example (`src/controllers/test/definitions.ts`):**
|
|
133
133
|
```typescript
|
|
134
|
-
export const
|
|
135
|
-
|
|
134
|
+
export const RouteConfigs = {
|
|
135
|
+
GET_USERS: { /* ... */ },
|
|
136
|
+
GET_USER_BY_ID: { /* ... */ },
|
|
136
137
|
} as const; // <-- This is crucial!
|
|
137
138
|
```
|
|
138
|
-
This ensures that `TRouteContext<typeof
|
|
139
|
+
This ensures that `TRouteContext<typeof RouteConfigs.GET_USERS>` has the precise types for request body, params, and response.
|
|
140
|
+
|
|
141
|
+
## 6. Bulk Operations Without WHERE Clause
|
|
142
|
+
|
|
143
|
+
**Problem:** Attempting to update or delete all records without an explicit `where` condition.
|
|
144
|
+
|
|
145
|
+
**Solution:** Ignis prevents accidental bulk data destruction. You must either provide a `where` condition or explicitly set `force: true`.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// ❌ BAD - Will throw error
|
|
149
|
+
await userRepository.updateBy({
|
|
150
|
+
data: { status: 'INACTIVE' },
|
|
151
|
+
where: {}, // Empty where = targets ALL records
|
|
152
|
+
});
|
|
153
|
+
// Error: [updateBy] DENY to perform updateBy | Empty where condition
|
|
154
|
+
|
|
155
|
+
// ✅ GOOD - Explicit where condition
|
|
156
|
+
await userRepository.updateBy({
|
|
157
|
+
data: { status: 'INACTIVE' },
|
|
158
|
+
where: { lastLoginAt: { lt: new Date('2024-01-01') } },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ✅ GOOD - Intentionally affect all records with force flag
|
|
162
|
+
await userRepository.updateBy({
|
|
163
|
+
data: { status: 'INACTIVE' },
|
|
164
|
+
where: {},
|
|
165
|
+
options: { force: true }, // Explicitly allow empty where
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> [!WARNING]
|
|
170
|
+
> The `force: true` flag bypasses the safety check. Only use when you intentionally want to affect ALL records in the table.
|
|
171
|
+
|
|
172
|
+
## 7. Schema Key Mismatch
|
|
173
|
+
|
|
174
|
+
**Problem:** Entity name doesn't match the table name registered in the DataSource's schema.
|
|
175
|
+
|
|
176
|
+
**Error Message:**
|
|
177
|
+
```
|
|
178
|
+
[UserRepository] Schema key mismatch | Entity name 'User' not found in connector.query | Available keys: [Configuration, Post]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Solution:** Ensure your entity class name matches the table name in `pgTable()`:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// ❌ BAD - Class name 'User' doesn't match table name 'users'
|
|
185
|
+
@model({ type: 'entity' })
|
|
186
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
187
|
+
static override schema = pgTable('users', { /* ... */ }); // Lowercase 'users'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ✅ GOOD - Class name matches table name
|
|
191
|
+
@model({ type: 'entity' })
|
|
192
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
193
|
+
static override schema = pgTable('User', { /* ... */ }); // Matches class name
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Why this matters:** The framework uses `entity.name` (class name) to look up the query interface in `connector.query`. If they don't match, the repository can't find its table.
|
|
198
|
+
|
|
199
|
+
## 8. Validation Error Response Structure
|
|
200
|
+
|
|
201
|
+
**Problem:** Client receives validation errors but doesn't know how to parse them.
|
|
202
|
+
|
|
203
|
+
**Solution:** Understand the Zod validation error response format:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"statusCode": 422,
|
|
208
|
+
"message": "ValidationError",
|
|
209
|
+
"requestId": "abc123",
|
|
210
|
+
"details": {
|
|
211
|
+
"cause": [
|
|
212
|
+
{
|
|
213
|
+
"path": "email",
|
|
214
|
+
"message": "Invalid email",
|
|
215
|
+
"code": "invalid_string",
|
|
216
|
+
"expected": "email",
|
|
217
|
+
"received": "string"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"path": "age",
|
|
221
|
+
"message": "Expected number, received string",
|
|
222
|
+
"code": "invalid_type",
|
|
223
|
+
"expected": "number",
|
|
224
|
+
"received": "string"
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Client-side handling:**
|
|
232
|
+
```typescript
|
|
233
|
+
try {
|
|
234
|
+
await api.post('/users', data);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error.response?.status === 422) {
|
|
237
|
+
const errors = error.response.data.details.cause;
|
|
238
|
+
errors.forEach(err => {
|
|
239
|
+
console.log(`Field '${err.path}': ${err.message}`);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
@@ -40,6 +40,32 @@ make install
|
|
|
40
40
|
git remote add upstream https://github.com/VENIZIA-AI/ignis.git
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## Package Build Order
|
|
44
|
+
|
|
45
|
+
Ignis is a monorepo with interdependent packages. Understanding the dependency chain is critical for development:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
dev-configs → inversion → helpers → boot → core
|
|
49
|
+
↓ ↓ ↓ ↓ ↓
|
|
50
|
+
@venizia/ @venizia/ @venizia/ @venizia/ @venizia/
|
|
51
|
+
dev-configs ignis- ignis- ignis- ignis
|
|
52
|
+
inversion helpers boot (core)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Dependency meanings:**
|
|
56
|
+
| Package | Depends On | Purpose |
|
|
57
|
+
|---------|------------|---------|
|
|
58
|
+
| `dev-configs` | - | Shared ESLint, TypeScript configs |
|
|
59
|
+
| `inversion` | dev-configs | IoC container, DI primitives |
|
|
60
|
+
| `helpers` | inversion | Utilities, loggers, crypto |
|
|
61
|
+
| `boot` | helpers | Application bootstrapping |
|
|
62
|
+
| `core` | boot | Full framework (controllers, repos, etc.) |
|
|
63
|
+
|
|
64
|
+
**Why this matters:**
|
|
65
|
+
- If you modify `helpers`, you must rebuild `boot` and `core`
|
|
66
|
+
- If you modify `inversion`, you must rebuild `helpers`, `boot`, and `core`
|
|
67
|
+
- The Makefile handles this automatically with dependencies
|
|
68
|
+
|
|
43
69
|
## Makefile Commands
|
|
44
70
|
|
|
45
71
|
The project uses a Makefile for common development tasks:
|
|
@@ -53,14 +79,15 @@ The project uses a Makefile for common development tasks:
|
|
|
53
79
|
| `make lint` | Lint all packages |
|
|
54
80
|
| `make help` | Show all available commands |
|
|
55
81
|
|
|
56
|
-
**Individual package builds
|
|
82
|
+
**Individual package builds** (dependencies are automatically resolved):
|
|
57
83
|
```bash
|
|
58
|
-
make core # Build @venizia/ignis (
|
|
59
|
-
make boot # Build @venizia/ignis-boot
|
|
60
|
-
make helpers # Build @venizia/ignis-helpers
|
|
61
|
-
make inversion # Build @venizia/ignis-inversion
|
|
62
|
-
make dev-configs # Build @venizia/dev-configs
|
|
63
|
-
make docs # Build documentation
|
|
84
|
+
make core # Build @venizia/ignis (builds dev-configs → inversion → helpers → boot → core)
|
|
85
|
+
make boot # Build @venizia/ignis-boot (builds dev-configs → inversion → helpers → boot)
|
|
86
|
+
make helpers # Build @venizia/ignis-helpers (builds dev-configs → inversion → helpers)
|
|
87
|
+
make inversion # Build @venizia/ignis-inversion (builds dev-configs → inversion)
|
|
88
|
+
make dev-configs # Build @venizia/dev-configs only
|
|
89
|
+
make docs # Build VitePress documentation (independent)
|
|
90
|
+
make docs-mcp # Build MCP documentation server
|
|
64
91
|
```
|
|
65
92
|
|
|
66
93
|
**Force update individual packages:**
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Data Modeling
|
|
2
|
+
|
|
3
|
+
Ignis streamlines data modeling with Drizzle ORM by providing powerful helpers and "enrichers" that reduce boilerplate code for common schema patterns.
|
|
4
|
+
|
|
5
|
+
## 1. Base Entity
|
|
6
|
+
|
|
7
|
+
All entity models should extend `BaseEntity`. This provides integration with the framework's repository layer and automatic schema generation support.
|
|
8
|
+
|
|
9
|
+
The recommended pattern is to define the schema and relations as **static properties** on the class. This keeps the definition self-contained and enables powerful type inference.
|
|
10
|
+
|
|
11
|
+
**Example (`src/models/entities/user.model.ts`):**
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { BaseEntity, extraUserColumns, generateIdColumnDefs, model } from '@venizia/ignis';
|
|
15
|
+
import { pgTable } from 'drizzle-orm/pg-core';
|
|
16
|
+
|
|
17
|
+
@model({ type: 'entity' })
|
|
18
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
19
|
+
// 1. Define schema as a static property
|
|
20
|
+
static override schema = pgTable('User', {
|
|
21
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
22
|
+
...extraUserColumns({ idType: 'string' }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// 2. Define relations as a static method (return empty array if none)
|
|
26
|
+
static override relations = () => [];
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 2. Schema Enrichers
|
|
31
|
+
|
|
32
|
+
Instead of manually defining common columns like primary keys, timestamps, or audit fields in every table, use Ignis "enrichers".
|
|
33
|
+
|
|
34
|
+
**Available Enrichers:**
|
|
35
|
+
|
|
36
|
+
| Enricher | Description | Columns Added |
|
|
37
|
+
|----------|-------------|---------------|
|
|
38
|
+
| `generateIdColumnDefs` | Adds a Primary Key | `id` (text, number, or big-number) |
|
|
39
|
+
| `generatePrincipalColumnDefs` | Adds polymorphic relation fields | `{discriminator}Id`, `{discriminator}Type` |
|
|
40
|
+
| `generateTzColumnDefs` | Adds timestamps | `createdAt`, `modifiedAt` (auto-updating) |
|
|
41
|
+
| `generateUserAuditColumnDefs` | Adds audit fields | `createdBy`, `modifiedBy` |
|
|
42
|
+
| `generateDataTypeColumnDefs` | Adds generic value fields | `nValue` (number), `tValue` (text), `jValue` (json), etc. |
|
|
43
|
+
| `extraUserColumns` | Comprehensive user fields | Combines audit, timestamps, status, and type fields |
|
|
44
|
+
|
|
45
|
+
**Usage Example:**
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import {
|
|
49
|
+
generateIdColumnDefs,
|
|
50
|
+
generateTzColumnDefs,
|
|
51
|
+
generateUserAuditColumnDefs,
|
|
52
|
+
} from '@venizia/ignis';
|
|
53
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
54
|
+
|
|
55
|
+
export const configurationTable = pgTable(
|
|
56
|
+
'Configuration',
|
|
57
|
+
{
|
|
58
|
+
// 1. Auto-generate text Primary Key with UUID default
|
|
59
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
60
|
+
|
|
61
|
+
// 2. Auto-generate createdAt / modifiedAt
|
|
62
|
+
...generateTzColumnDefs(),
|
|
63
|
+
|
|
64
|
+
// 3. Auto-generate createdBy / modifiedBy
|
|
65
|
+
...generateUserAuditColumnDefs({
|
|
66
|
+
created: { dataType: 'string', columnName: 'created_by' },
|
|
67
|
+
modified: { dataType: 'string', columnName: 'modified_by' },
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
// 4. Your custom columns
|
|
71
|
+
code: text('code').notNull(),
|
|
72
|
+
description: text('description'),
|
|
73
|
+
group: text('group').notNull(),
|
|
74
|
+
},
|
|
75
|
+
(table) => [
|
|
76
|
+
// Define indexes/constraints here
|
|
77
|
+
unique('UQ_code').on(table.code),
|
|
78
|
+
]
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### ID Type Options
|
|
83
|
+
|
|
84
|
+
The `generateIdColumnDefs` enricher supports multiple ID strategies:
|
|
85
|
+
|
|
86
|
+
| Data Type | PostgreSQL Type | JavaScript Type | Use Case |
|
|
87
|
+
|-----------|-----------------|-----------------|----------|
|
|
88
|
+
| `string` | `TEXT` | `string` | UUIDs, custom IDs, distributed systems |
|
|
89
|
+
| `number` | `INTEGER GENERATED ALWAYS AS IDENTITY` | `number` | Auto-increment, simple sequences |
|
|
90
|
+
| `big-number` (mode: `number`) | `BIGINT GENERATED ALWAYS AS IDENTITY` | `number` | Large sequences (up to 2^53) |
|
|
91
|
+
| `big-number` (mode: `bigint`) | `BIGINT GENERATED ALWAYS AS IDENTITY` | `bigint` | Very large sequences (up to 2^64) |
|
|
92
|
+
|
|
93
|
+
**Examples:**
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// String ID with default UUID generator
|
|
97
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } })
|
|
98
|
+
// Result: id TEXT PRIMARY KEY DEFAULT crypto.randomUUID()
|
|
99
|
+
|
|
100
|
+
// String ID with custom generator (e.g., nanoid, ulid)
|
|
101
|
+
import { nanoid } from 'nanoid';
|
|
102
|
+
...generateIdColumnDefs({ id: { dataType: 'string', generator: () => nanoid() } })
|
|
103
|
+
|
|
104
|
+
// Auto-increment integer
|
|
105
|
+
...generateIdColumnDefs({ id: { dataType: 'number' } })
|
|
106
|
+
// Result: id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY
|
|
107
|
+
|
|
108
|
+
// Big integer for large datasets (JavaScript number - up to 2^53)
|
|
109
|
+
...generateIdColumnDefs({ id: { dataType: 'big-number', numberMode: 'number' } })
|
|
110
|
+
|
|
111
|
+
// Big integer with native BigInt (up to 2^64)
|
|
112
|
+
...generateIdColumnDefs({ id: { dataType: 'big-number', numberMode: 'bigint' } })
|
|
113
|
+
|
|
114
|
+
// With sequence options
|
|
115
|
+
...generateIdColumnDefs({
|
|
116
|
+
id: {
|
|
117
|
+
dataType: 'number',
|
|
118
|
+
sequenceOptions: { startWith: 1000, increment: 1 },
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Principal Enricher (Polymorphic Relations)
|
|
124
|
+
|
|
125
|
+
Use `generatePrincipalColumnDefs` when a record can belong to different entity types (polymorphic relationship).
|
|
126
|
+
|
|
127
|
+
**Use Case:** A `Comment` can belong to either a `Post` or a `Product`.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { generateIdColumnDefs, generatePrincipalColumnDefs } from '@venizia/ignis';
|
|
131
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
132
|
+
|
|
133
|
+
export const commentTable = pgTable('Comment', {
|
|
134
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
135
|
+
|
|
136
|
+
// Polymorphic relation: commentable can be Post or Product
|
|
137
|
+
...generatePrincipalColumnDefs({
|
|
138
|
+
discriminator: 'commentable', // Field prefix
|
|
139
|
+
polymorphicIdType: 'string', // ID type of related entities
|
|
140
|
+
defaultPolymorphic: 'Post', // Default type
|
|
141
|
+
}),
|
|
142
|
+
|
|
143
|
+
content: text('content').notNull(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Generated columns:
|
|
147
|
+
// - commentableId: TEXT NOT NULL
|
|
148
|
+
// - commentableType: TEXT DEFAULT 'Post'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Querying polymorphic relations:**
|
|
152
|
+
```typescript
|
|
153
|
+
// Find all comments on a specific post
|
|
154
|
+
const comments = await commentRepo.find({
|
|
155
|
+
filter: {
|
|
156
|
+
where: {
|
|
157
|
+
commentableType: 'Post',
|
|
158
|
+
commentableId: postId,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Find all comments on a product
|
|
164
|
+
const productComments = await commentRepo.find({
|
|
165
|
+
filter: {
|
|
166
|
+
where: {
|
|
167
|
+
commentableType: 'Product',
|
|
168
|
+
commentableId: productId,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## 3. Defining Relations
|
|
175
|
+
|
|
176
|
+
Relations are defined using the `TRelationConfig` structure within the static `relations` method of your model.
|
|
177
|
+
|
|
178
|
+
### Relation Types
|
|
179
|
+
|
|
180
|
+
| Type | Constant | Description | Example |
|
|
181
|
+
|------|----------|-------------|---------|
|
|
182
|
+
| One-to-One | `RelationTypes.ONE` | Single related record | User → Profile |
|
|
183
|
+
| One-to-Many | `RelationTypes.MANY` | Multiple related records | User → Posts |
|
|
184
|
+
|
|
185
|
+
### Basic Relations
|
|
186
|
+
|
|
187
|
+
**One-to-One (belongsTo):**
|
|
188
|
+
```typescript
|
|
189
|
+
import { BaseEntity, model, RelationTypes, TRelationConfig } from '@venizia/ignis';
|
|
190
|
+
import { User } from './user.model';
|
|
191
|
+
|
|
192
|
+
@model({ type: 'entity' })
|
|
193
|
+
export class Configuration extends BaseEntity<typeof Configuration.schema> {
|
|
194
|
+
static override schema = pgTable('Configuration', {
|
|
195
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
196
|
+
createdBy: text('created_by'),
|
|
197
|
+
// ...
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Define relations
|
|
201
|
+
static override relations = (): TRelationConfig[] => [
|
|
202
|
+
{
|
|
203
|
+
name: 'creator', // Relation name used in include
|
|
204
|
+
type: RelationTypes.ONE, // One Configuration → One User
|
|
205
|
+
schema: User.schema, // Related entity's schema
|
|
206
|
+
metadata: {
|
|
207
|
+
fields: [Configuration.schema.createdBy], // Foreign key
|
|
208
|
+
references: [User.schema.id], // Primary key
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**One-to-Many (hasMany):**
|
|
216
|
+
```typescript
|
|
217
|
+
@model({ type: 'entity' })
|
|
218
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
219
|
+
static override schema = pgTable('User', {
|
|
220
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
221
|
+
name: text('name').notNull(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
static override relations = (): TRelationConfig[] => [
|
|
225
|
+
{
|
|
226
|
+
name: 'posts', // User.posts
|
|
227
|
+
type: RelationTypes.MANY, // One User → Many Posts
|
|
228
|
+
schema: Post.schema,
|
|
229
|
+
metadata: {
|
|
230
|
+
fields: [User.schema.id],
|
|
231
|
+
references: [Post.schema.authorId],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'comments', // User.comments
|
|
236
|
+
type: RelationTypes.MANY,
|
|
237
|
+
schema: Comment.schema,
|
|
238
|
+
metadata: {
|
|
239
|
+
fields: [User.schema.id],
|
|
240
|
+
references: [Comment.schema.userId],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Using Relations in Queries
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Eager load single relation
|
|
251
|
+
const configs = await configRepo.find({
|
|
252
|
+
filter: {
|
|
253
|
+
include: [{ relation: 'creator' }],
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
// Result: [{ id, code, ..., creator: { id, name, email } }]
|
|
257
|
+
|
|
258
|
+
// Eager load multiple relations
|
|
259
|
+
const users = await userRepo.find({
|
|
260
|
+
filter: {
|
|
261
|
+
include: [
|
|
262
|
+
{ relation: 'posts' },
|
|
263
|
+
{ relation: 'comments' },
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Nested relations (up to 2 levels recommended)
|
|
269
|
+
const users = await userRepo.find({
|
|
270
|
+
filter: {
|
|
271
|
+
include: [{
|
|
272
|
+
relation: 'posts',
|
|
273
|
+
scope: {
|
|
274
|
+
include: [{ relation: 'comments' }],
|
|
275
|
+
},
|
|
276
|
+
}],
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
> [!TIP]
|
|
282
|
+
> Avoid deeply nested includes (more than 2 levels). Each level adds query complexity. For complex data fetching, consider separate queries.
|
|
283
|
+
|
|
284
|
+
## 4. Repositories and Auto-Discovery
|
|
285
|
+
|
|
286
|
+
Ignis simplifies the connection between models, repositories, and datasources.
|
|
287
|
+
|
|
288
|
+
### DataSource Auto-Discovery
|
|
289
|
+
|
|
290
|
+
DataSources automatically discover their schema from the repositories that bind to them. You **do not** need to manually register schemas in the DataSource constructor.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// src/datasources/postgres.datasource.ts
|
|
294
|
+
@datasource({ driver: 'node-postgres' })
|
|
295
|
+
export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
|
|
296
|
+
constructor() {
|
|
297
|
+
super({
|
|
298
|
+
name: PostgresDataSource.name,
|
|
299
|
+
config: { /* connection config */ },
|
|
300
|
+
// NO schema property needed - auto-discovered!
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
override configure(): ValueOrPromise<void> {
|
|
305
|
+
// This method automatically collects all schemas from bound repositories
|
|
306
|
+
const schema = this.getSchema();
|
|
307
|
+
this.connector = drizzle({ client: new Pool(this.settings), schema });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Repository Binding
|
|
313
|
+
|
|
314
|
+
Repositories use the `@repository` decorator to bind a **Model** to a **DataSource**. This binding is what powers the auto-discovery mechanism.
|
|
315
|
+
|
|
316
|
+
**Pattern 1: Zero Boilerplate (Recommended)**
|
|
317
|
+
|
|
318
|
+
For most repositories, you don't need a constructor. The DataSource is automatically injected.
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
@repository({ model: Configuration, dataSource: PostgresDataSource })
|
|
322
|
+
export class ConfigurationRepository extends DefaultCRUDRepository<typeof Configuration.schema> {
|
|
323
|
+
// No constructor needed!
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Pattern 2: Explicit Injection (Advanced)**
|
|
328
|
+
|
|
329
|
+
If you need to perform custom initialization or inject additional dependencies, you can define a constructor. **Important:** The first parameter must be the DataSource.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
@repository({ model: User, dataSource: PostgresDataSource })
|
|
333
|
+
export class UserRepository extends ReadableRepository<typeof User.schema> {
|
|
334
|
+
constructor(
|
|
335
|
+
@inject({ key: 'datasources.PostgresDataSource' })
|
|
336
|
+
dataSource: PostgresDataSource,
|
|
337
|
+
) {
|
|
338
|
+
super(dataSource);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Custom methods
|
|
342
|
+
async findByRealm(realm: string) {
|
|
343
|
+
return this.findOne({ filter: { where: { realm } } });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## 5. Hidden Properties
|
|
349
|
+
|
|
350
|
+
Protect sensitive data by configuring properties that are excluded at the SQL level. Hidden properties are **never returned** through repository queries.
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
@model({
|
|
354
|
+
type: 'entity',
|
|
355
|
+
settings: {
|
|
356
|
+
hiddenProperties: ['password', 'secret'],
|
|
357
|
+
},
|
|
358
|
+
})
|
|
359
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
360
|
+
static override schema = pgTable('User', {
|
|
361
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
362
|
+
email: text('email').notNull(),
|
|
363
|
+
password: text('password'), // Never returned via repository
|
|
364
|
+
secret: text('secret'), // Never returned via repository
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Key points:**
|
|
370
|
+
|
|
371
|
+
- Hidden properties are excluded from SELECT, INSERT RETURNING, UPDATE RETURNING, DELETE RETURNING
|
|
372
|
+
- You can still **filter by** hidden properties in where clauses
|
|
373
|
+
- Hidden properties are **recursively excluded** from included relations
|
|
374
|
+
- Use the connector directly when you need to access hidden data (e.g., password verification)
|
|
375
|
+
|
|
376
|
+
> **Reference:** See [Hidden Properties](../references/base/models.md#hidden-properties) for complete documentation.
|