@venizia/ignis-docs 0.0.1-9 → 0.0.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/LICENSE.md +1 -0
- package/package.json +2 -2
- package/wiki/changelogs/{v0.0.1-7-initial-architecture.md → 2025-12-16-initial-architecture.md} +20 -12
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +300 -0
- package/wiki/changelogs/2025-12-17-refactor.md +80 -12
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +28 -90
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +101 -297
- package/wiki/changelogs/index.md +20 -8
- package/wiki/changelogs/planned-schema-migrator.md +561 -0
- package/wiki/changelogs/planned-transaction-support.md +216 -0
- package/wiki/changelogs/template.md +123 -0
- package/wiki/get-started/best-practices/api-usage-examples.md +0 -2
- package/wiki/get-started/best-practices/architectural-patterns.md +2 -2
- package/wiki/get-started/best-practices/code-style-standards.md +575 -10
- package/wiki/get-started/best-practices/common-pitfalls.md +5 -3
- package/wiki/get-started/best-practices/contribution-workflow.md +2 -0
- package/wiki/get-started/best-practices/data-modeling.md +91 -34
- package/wiki/get-started/best-practices/security-guidelines.md +3 -1
- package/wiki/get-started/building-a-crud-api.md +3 -3
- package/wiki/get-started/core-concepts/application.md +72 -3
- package/wiki/get-started/core-concepts/bootstrapping.md +566 -0
- package/wiki/get-started/core-concepts/components.md +4 -2
- package/wiki/get-started/core-concepts/persistent.md +350 -378
- package/wiki/get-started/core-concepts/services.md +21 -27
- package/wiki/references/base/bootstrapping.md +789 -0
- package/wiki/references/base/components.md +1 -1
- package/wiki/references/base/dependency-injection.md +95 -2
- package/wiki/references/base/services.md +2 -2
- package/wiki/references/components/authentication.md +4 -3
- package/wiki/references/components/index.md +1 -1
- package/wiki/references/helpers/error.md +2 -2
- package/wiki/references/src-details/boot.md +379 -0
- package/wiki/references/src-details/core.md +2 -2
- package/wiki/changelogs/v0.0.1-8-model-repo-datasource-refactor.md +0 -278
|
@@ -1,313 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Persistent Layer
|
|
3
|
+
description: Models, DataSources, and Repositories in Ignis
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Persistent Layer: Models, DataSources, and Repositories
|
|
2
7
|
|
|
3
8
|
The persistent layer manages data using [Drizzle ORM](https://orm.drizzle.team/) for type-safe database access and the Repository pattern for data abstraction.
|
|
4
9
|
|
|
5
10
|
**Three main components:**
|
|
6
|
-
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
11
|
+
|
|
12
|
+
- **Models** - Define data structure (static schema + relations on Entity class)
|
|
13
|
+
- **DataSources** - Manage database connections with auto-discovery
|
|
14
|
+
- **Repositories** - Provide CRUD operations with zero boilerplate
|
|
9
15
|
|
|
10
16
|
## 1. Models: Defining Your Data Structure
|
|
11
17
|
|
|
12
|
-
A model in
|
|
18
|
+
A model in Ignis is a single class with static properties for schema and relations. No separate variables needed.
|
|
13
19
|
|
|
14
20
|
### Creating a Basic Model
|
|
15
21
|
|
|
16
|
-
Here's how to create a simple `User` model.
|
|
17
|
-
|
|
18
22
|
```typescript
|
|
19
23
|
// src/models/entities/user.model.ts
|
|
20
|
-
import {
|
|
21
|
-
BaseEntity,
|
|
22
|
-
createRelations,
|
|
23
|
-
extraUserColumns,
|
|
24
|
-
generateIdColumnDefs,
|
|
25
|
-
model,
|
|
26
|
-
TTableObject,
|
|
27
|
-
} from '@venizia/ignis';
|
|
24
|
+
import { BaseEntity, extraUserColumns, generateIdColumnDefs, model } from '@venizia/ignis';
|
|
28
25
|
import { pgTable } from 'drizzle-orm/pg-core';
|
|
29
26
|
|
|
30
|
-
// 1. Define the Drizzle schema for the 'User' table
|
|
31
|
-
export const userTable = pgTable(User.name, {
|
|
32
|
-
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
33
|
-
...extraUserColumns({ idType: 'string' }),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// 2. Define relations (empty for now, but required)
|
|
37
|
-
export const userRelations = createRelations({
|
|
38
|
-
source: userTable,
|
|
39
|
-
relations: [],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// 3. Define the TypeScript type for a User object
|
|
43
|
-
export type TUserSchema = typeof userTable;
|
|
44
|
-
export type TUser = TTableObject<TUserSchema>;
|
|
45
|
-
|
|
46
|
-
// 4. Create the Entity class, decorated with @model
|
|
47
27
|
@model({ type: 'entity' })
|
|
48
28
|
export class User extends BaseEntity<typeof User.schema> {
|
|
49
|
-
|
|
50
|
-
static override
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
### Understanding Enrichers: The Smart Column Generators
|
|
56
|
-
|
|
57
|
-
You might have noticed functions like `generateIdColumnDefs()` and `extraUserColumns()` in the model definition. These are **Enrichers**—powerful helper functions that generate common database columns automatically.
|
|
58
|
-
|
|
59
|
-
#### Why Enrichers Exist
|
|
60
|
-
|
|
61
|
-
**Without enrichers (the hard way):**
|
|
62
|
-
```typescript
|
|
63
|
-
export const userTable = pgTable('User', {
|
|
64
|
-
// Manually define every common column in every table
|
|
65
|
-
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
66
|
-
status: text('status').notNull().default('ACTIVE'),
|
|
67
|
-
type: text('type'),
|
|
68
|
-
createdBy: text('created_by'),
|
|
69
|
-
modifiedBy: text('modified_by'),
|
|
70
|
-
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
71
|
-
modifiedAt: timestamp('modified_at', { withTimezone: true }).notNull().defaultNow(),
|
|
72
|
-
// ... your actual user-specific fields
|
|
73
|
-
email: text('email').notNull(),
|
|
74
|
-
name: text('name'),
|
|
75
|
-
});
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
**With enrichers (the smart way):**
|
|
79
|
-
```typescript
|
|
80
|
-
export const userTable = pgTable('User', {
|
|
81
|
-
...generateIdColumnDefs({ id: { dataType: 'string' } }), // Adds: id (UUID)
|
|
82
|
-
...extraUserColumns({ idType: 'string' }), // Adds: status, type, createdBy, modifiedBy, createdAt, modifiedAt
|
|
83
|
-
// Just your actual user-specific fields
|
|
84
|
-
email: text('email').notNull(),
|
|
85
|
-
name: text('name'),
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
**Result:** Same table structure, but with:
|
|
90
|
-
- 7 fewer lines of code
|
|
91
|
-
- Guaranteed consistency across all tables
|
|
92
|
-
- Less chance of typos or mistakes
|
|
93
|
-
- Easier to maintain
|
|
94
|
-
|
|
95
|
-
#### Common Enrichers
|
|
96
|
-
|
|
97
|
-
| Enricher | What It Adds | Use Case |
|
|
98
|
-
| :--- | :--- | :--- |
|
|
99
|
-
| `generateIdColumnDefs()` | Primary key `id` column (UUID or number) | Every table needs an ID |
|
|
100
|
-
| `generateTzColumnDefs()` | `createdAt` and `modifiedAt` timestamps | Track when records are created/updated |
|
|
101
|
-
| `generateUserAuditColumnDefs()` | `createdBy` and `modifiedBy` foreign keys | Track which user created/updated records |
|
|
102
|
-
| `generateDataTypeColumnDefs()` | `dataType` and type-specific value columns (`tValue`, `nValue`, etc.) | Configuration tables with mixed data types |
|
|
103
|
-
| `extraUserColumns()` | Combination of audit + status + type fields | Full-featured entity tables |
|
|
104
|
-
|
|
105
|
-
#### Practical Example: Building a Post Model
|
|
106
|
-
|
|
107
|
-
Let's create a blog post model using enrichers:
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// src/models/post.model.ts
|
|
111
|
-
import {
|
|
112
|
-
BaseEntity,
|
|
113
|
-
createRelations,
|
|
114
|
-
generateIdColumnDefs,
|
|
115
|
-
generateTzColumnDefs,
|
|
116
|
-
generateUserAuditColumnDefs,
|
|
117
|
-
model,
|
|
118
|
-
RelationTypes,
|
|
119
|
-
TTableObject,
|
|
120
|
-
} from '@venizia/ignis';
|
|
121
|
-
import { pgTable, text, boolean } from 'drizzle-orm/pg-core';
|
|
122
|
-
import { userTable } from './user.model';
|
|
123
|
-
|
|
124
|
-
export const postTable = pgTable('Post', {
|
|
125
|
-
// Use enrichers for common columns
|
|
126
|
-
...generateIdColumnDefs({ id: { dataType: 'string' } }), // id: UUID primary key
|
|
127
|
-
...generateTzColumnDefs(), // createdAt, modifiedAt
|
|
128
|
-
...generateUserAuditColumnDefs({ // createdBy, modifiedBy
|
|
129
|
-
created: { dataType: 'string', columnName: 'created_by' },
|
|
130
|
-
modified: { dataType: 'string', columnName: 'modified_by' },
|
|
131
|
-
}),
|
|
132
|
-
|
|
133
|
-
// Your post-specific fields
|
|
134
|
-
title: text('title').notNull(),
|
|
135
|
-
content: text('content').notNull(),
|
|
136
|
-
isPublished: boolean('is_published').default(false),
|
|
137
|
-
slug: text('slug').notNull().unique(),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
export const postRelations = createRelations({
|
|
141
|
-
source: postTable,
|
|
142
|
-
relations: [
|
|
143
|
-
{
|
|
144
|
-
name: 'author',
|
|
145
|
-
type: RelationTypes.ONE,
|
|
146
|
-
schema: userTable,
|
|
147
|
-
metadata: {
|
|
148
|
-
fields: [postTable.createdBy],
|
|
149
|
-
references: [userTable.id],
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
export type TPostSchema = typeof postTable;
|
|
156
|
-
export type TPost = TTableObject<TPostSchema>;
|
|
157
|
-
|
|
158
|
-
@model({ type: 'entity' })
|
|
159
|
-
export class Post extends BaseEntity<typeof Post.schema> {
|
|
160
|
-
static override schema = postTable;
|
|
161
|
-
static override relations = () => postRelations.definitions;
|
|
162
|
-
static override TABLE_NAME = 'Post';
|
|
163
|
-
}
|
|
164
|
-
```
|
|
29
|
+
// Define schema as static property
|
|
30
|
+
static override schema = pgTable('User', {
|
|
31
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
32
|
+
...extraUserColumns({ idType: 'string' }),
|
|
33
|
+
});
|
|
165
34
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
interface Post {
|
|
169
|
-
id: string; // From generateIdColumnDefs
|
|
170
|
-
createdAt: Date; // From generateTzColumnDefs
|
|
171
|
-
modifiedAt: Date; // From generateTzColumnDefs
|
|
172
|
-
createdBy: string; // From generateUserAuditColumnDefs
|
|
173
|
-
modifiedBy: string; // From generateUserAuditColumnDefs
|
|
174
|
-
title: string; // Your field
|
|
175
|
-
content: string; // Your field
|
|
176
|
-
isPublished: boolean; // Your field
|
|
177
|
-
slug: string; // Your field
|
|
35
|
+
// Relations (empty array if none)
|
|
36
|
+
static override relations = () => [];
|
|
178
37
|
}
|
|
179
38
|
```
|
|
180
39
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
You can always define columns manually if:
|
|
184
|
-
- You need a custom ID strategy (e.g., integer auto-increment)
|
|
185
|
-
- You don't need audit fields for a specific table
|
|
186
|
-
- You have very specific timestamp requirements
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
// Mixing enrichers with manual columns is perfectly fine
|
|
190
|
-
export const simpleTable = pgTable('Simple', {
|
|
191
|
-
...generateIdColumnDefs({ id: { dataType: 'number' } }), // Use enricher for ID
|
|
192
|
-
// But manually define everything else
|
|
193
|
-
name: text('name').notNull(),
|
|
194
|
-
value: integer('value'),
|
|
195
|
-
});
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
:::tip
|
|
199
|
-
For a complete list of available enrichers and their options, see the [**Schema Enrichers Reference**](../../references/base/models.md#schema-enrichers).
|
|
200
|
-
:::
|
|
40
|
+
**Key points:**
|
|
201
41
|
|
|
202
|
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
- **`BaseEntity`**: The class your model extends. It wraps the Drizzle schema and provides utilities for the framework.
|
|
207
|
-
- **`@model`**: A decorator that registers the class with the framework as a database model.
|
|
42
|
+
- Schema is defined inline as `static override schema`
|
|
43
|
+
- Relations are defined as `static override relations`
|
|
44
|
+
- No constructor needed - BaseEntity auto-discovers from static properties
|
|
45
|
+
- Type parameter uses `typeof User.schema` (self-referencing)
|
|
208
46
|
|
|
209
47
|
### Creating a Model with Relations
|
|
210
48
|
|
|
211
|
-
Now, let's create a `Configuration` model that has a relationship with the `User` model.
|
|
212
|
-
|
|
213
49
|
```typescript
|
|
214
50
|
// src/models/entities/configuration.model.ts
|
|
215
51
|
import {
|
|
216
52
|
BaseEntity,
|
|
217
|
-
createRelations, // Import createRelations
|
|
218
53
|
generateDataTypeColumnDefs,
|
|
219
54
|
generateIdColumnDefs,
|
|
220
55
|
generateTzColumnDefs,
|
|
221
56
|
generateUserAuditColumnDefs,
|
|
222
57
|
model,
|
|
223
58
|
RelationTypes,
|
|
224
|
-
|
|
59
|
+
TRelationConfig,
|
|
225
60
|
} from '@venizia/ignis';
|
|
226
61
|
import { foreignKey, index, pgTable, text, unique } from 'drizzle-orm/pg-core';
|
|
227
|
-
import { User
|
|
62
|
+
import { User } from './user.model';
|
|
228
63
|
|
|
229
|
-
|
|
230
|
-
export
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
relations: [
|
|
64
|
+
@model({ type: 'entity' })
|
|
65
|
+
export class Configuration extends BaseEntity<typeof Configuration.schema> {
|
|
66
|
+
static override schema = pgTable(
|
|
67
|
+
'Configuration',
|
|
68
|
+
{
|
|
69
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
70
|
+
...generateTzColumnDefs(),
|
|
71
|
+
...generateDataTypeColumnDefs(),
|
|
72
|
+
...generateUserAuditColumnDefs({
|
|
73
|
+
created: { dataType: 'string', columnName: 'created_by' },
|
|
74
|
+
modified: { dataType: 'string', columnName: 'modified_by' },
|
|
75
|
+
}),
|
|
76
|
+
code: text('code').notNull(),
|
|
77
|
+
description: text('description'),
|
|
78
|
+
group: text('group').notNull(),
|
|
79
|
+
},
|
|
80
|
+
def => [
|
|
81
|
+
unique('UQ_Configuration_code').on(def.code),
|
|
82
|
+
index('IDX_Configuration_group').on(def.group),
|
|
83
|
+
foreignKey({
|
|
84
|
+
columns: [def.createdBy],
|
|
85
|
+
foreignColumns: [User.schema.id], // Reference User.schema, not a separate variable
|
|
86
|
+
name: 'FK_Configuration_createdBy_User_id',
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Define relations using TRelationConfig array
|
|
92
|
+
static override relations = (): TRelationConfig[] => [
|
|
259
93
|
{
|
|
260
94
|
name: 'creator',
|
|
261
95
|
type: RelationTypes.ONE,
|
|
262
|
-
schema:
|
|
96
|
+
schema: User.schema,
|
|
263
97
|
metadata: {
|
|
264
|
-
fields: [
|
|
265
|
-
references: [
|
|
98
|
+
fields: [Configuration.schema.createdBy],
|
|
99
|
+
references: [User.schema.id],
|
|
266
100
|
},
|
|
267
101
|
},
|
|
268
102
|
{
|
|
269
103
|
name: 'modifier',
|
|
270
104
|
type: RelationTypes.ONE,
|
|
271
|
-
schema:
|
|
105
|
+
schema: User.schema,
|
|
272
106
|
metadata: {
|
|
273
|
-
fields: [
|
|
274
|
-
references: [
|
|
107
|
+
fields: [Configuration.schema.modifiedBy],
|
|
108
|
+
references: [User.schema.id],
|
|
275
109
|
},
|
|
276
110
|
},
|
|
277
|
-
]
|
|
278
|
-
}
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
```
|
|
279
114
|
|
|
280
|
-
|
|
281
|
-
export type TConfigurationSchema = typeof configurationTable;
|
|
282
|
-
export type TConfiguration = TTableObject<TConfigurationSchema>;
|
|
115
|
+
**Key points:**
|
|
283
116
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
117
|
+
- Relations use `TRelationConfig[]` format directly
|
|
118
|
+
- Reference other models via `Model.schema` (e.g., `User.schema.id`)
|
|
119
|
+
- Relation names (`creator`, `modifier`) are used in queries with `include`
|
|
120
|
+
|
|
121
|
+
### Understanding Enrichers
|
|
122
|
+
|
|
123
|
+
Enrichers are helper functions that generate common database columns automatically.
|
|
124
|
+
|
|
125
|
+
**Without enrichers:**
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
static override schema = pgTable('User', {
|
|
129
|
+
id: uuid('id').defaultRandom().primaryKey(),
|
|
130
|
+
status: text('status').notNull().default('ACTIVE'),
|
|
131
|
+
createdBy: text('created_by'),
|
|
132
|
+
modifiedBy: text('modified_by'),
|
|
133
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
134
|
+
modifiedAt: timestamp('modified_at', { withTimezone: true }).notNull().defaultNow(),
|
|
135
|
+
// ... your fields
|
|
136
|
+
});
|
|
290
137
|
```
|
|
291
|
-
**Key Concepts:**
|
|
292
|
-
- **`createRelations`**: This helper function from `Ignis` simplifies defining Drizzle ORM relations. It creates both a Drizzle `relations` object (for querying) and a `definitions` object (for repository configuration). Here, we define `creator` and `modifier` relations from `Configuration` to `User`. The names (`creator`, `modifier`) are important, as they will be used when querying.
|
|
293
138
|
|
|
294
|
-
|
|
295
|
-
> - Explore the [**`BaseEntity`**](../../references/base/models.md#baseentity-class) class.
|
|
296
|
-
> - See all available [**Enrichers**](../../references/base/models.md#schema-enrichers) for schema generation.
|
|
139
|
+
**With enrichers:**
|
|
297
140
|
|
|
298
|
-
|
|
141
|
+
```typescript
|
|
142
|
+
static override schema = pgTable('User', {
|
|
143
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }), // id (UUID)
|
|
144
|
+
...extraUserColumns({ idType: 'string' }), // status, audit fields, timestamps
|
|
145
|
+
// ... your fields
|
|
146
|
+
});
|
|
147
|
+
```
|
|
299
148
|
|
|
300
|
-
|
|
149
|
+
#### Available Enrichers
|
|
301
150
|
|
|
302
|
-
|
|
151
|
+
| Enricher | Columns Added | Use Case |
|
|
152
|
+
|----------|---------------|----------|
|
|
153
|
+
| `generateIdColumnDefs()` | `id` (UUID or number) | Every table |
|
|
154
|
+
| `generateTzColumnDefs()` | `createdAt`, `modifiedAt` | Track timestamps |
|
|
155
|
+
| `generateUserAuditColumnDefs()` | `createdBy`, `modifiedBy` | Track who created/updated |
|
|
156
|
+
| `generateDataTypeColumnDefs()` | `dataType`, `tValue`, `nValue`, etc. | Configuration tables |
|
|
157
|
+
| `extraUserColumns()` | Combines audit + status + type | Full-featured entities |
|
|
303
158
|
|
|
304
|
-
|
|
159
|
+
:::tip
|
|
160
|
+
For a complete list of enrichers and options, see the [Schema Enrichers Reference](../../references/base/models.md#schema-enrichers).
|
|
161
|
+
:::
|
|
162
|
+
|
|
163
|
+
---
|
|
305
164
|
|
|
306
|
-
|
|
165
|
+
## 2. DataSources: Connecting to Your Database
|
|
307
166
|
|
|
308
|
-
|
|
167
|
+
A DataSource manages database connections and supports **schema auto-discovery** from repositories.
|
|
309
168
|
|
|
310
|
-
|
|
169
|
+
### Creating a DataSource
|
|
311
170
|
|
|
312
171
|
```typescript
|
|
313
172
|
// src/datasources/postgres.datasource.ts
|
|
@@ -321,13 +180,11 @@ import { drizzle } from 'drizzle-orm/node-postgres';
|
|
|
321
180
|
import { Pool } from 'pg';
|
|
322
181
|
|
|
323
182
|
interface IDSConfigs {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
database?: string;
|
|
330
|
-
};
|
|
183
|
+
host: string;
|
|
184
|
+
port: number;
|
|
185
|
+
database: string;
|
|
186
|
+
user: string;
|
|
187
|
+
password: string;
|
|
331
188
|
}
|
|
332
189
|
|
|
333
190
|
@datasource({ driver: 'node-postgres' })
|
|
@@ -336,205 +193,164 @@ export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, I
|
|
|
336
193
|
super({
|
|
337
194
|
name: PostgresDataSource.name,
|
|
338
195
|
config: {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
database: process.env.APP_ENV_POSTGRES_DATABASE,
|
|
345
|
-
},
|
|
196
|
+
host: process.env.POSTGRES_HOST ?? 'localhost',
|
|
197
|
+
port: +(process.env.POSTGRES_PORT ?? 5432),
|
|
198
|
+
database: process.env.POSTGRES_DATABASE ?? 'mydb',
|
|
199
|
+
user: process.env.POSTGRES_USER ?? 'postgres',
|
|
200
|
+
password: process.env.POSTGRES_PASSWORD ?? '',
|
|
346
201
|
},
|
|
347
|
-
// No schema needed - auto-discovered from @repository
|
|
202
|
+
// No schema needed - auto-discovered from @repository bindings!
|
|
348
203
|
});
|
|
349
204
|
}
|
|
350
205
|
|
|
351
206
|
override configure(): ValueOrPromise<void> {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
207
|
+
// getSchema() auto-discovers models from @repository bindings
|
|
208
|
+
const schema = this.getSchema();
|
|
209
|
+
|
|
210
|
+
this.logger.debug(
|
|
211
|
+
'[configure] Auto-discovered schema | Keys: %o',
|
|
212
|
+
Object.keys(schema),
|
|
213
|
+
);
|
|
357
214
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return this.connector;
|
|
215
|
+
const client = new Pool(this.settings);
|
|
216
|
+
this.connector = drizzle({ client, schema });
|
|
361
217
|
}
|
|
362
218
|
|
|
363
|
-
override
|
|
364
|
-
|
|
219
|
+
override getConnectionString(): ValueOrPromise<string> {
|
|
220
|
+
const { host, port, user, password, database } = this.settings;
|
|
221
|
+
return `postgresql://${user}:${password}@${host}:${port}/${database}`;
|
|
365
222
|
}
|
|
366
223
|
}
|
|
367
224
|
```
|
|
368
225
|
|
|
369
226
|
**How auto-discovery works:**
|
|
370
227
|
|
|
371
|
-
|
|
228
|
+
1. `@repository` decorators register model-datasource bindings
|
|
229
|
+
2. When `configure()` is called, `getSchema()` collects all bound models
|
|
230
|
+
3. Drizzle is initialized with the complete schema
|
|
372
231
|
|
|
373
|
-
|
|
374
|
-
@repository({ model: User, dataSource: PostgresDataSource })
|
|
375
|
-
export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
|
|
232
|
+
### Manual Schema (Optional)
|
|
376
233
|
|
|
377
|
-
|
|
378
|
-
export class ConfigurationRepository extends DefaultCRUDRepository<typeof Configuration.schema> {}
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
The framework automatically:
|
|
382
|
-
1. Registers each model-datasource binding
|
|
383
|
-
2. Builds the combined schema (tables + relations) when `getSchema()` is called
|
|
384
|
-
3. Makes all registered models available for relational queries
|
|
385
|
-
|
|
386
|
-
**Result:** You can use `include` queries without any manual schema configuration:
|
|
387
|
-
```typescript
|
|
388
|
-
const config = await configRepo.findOne({
|
|
389
|
-
filter: {
|
|
390
|
-
where: { id: '123' },
|
|
391
|
-
include: [{ relation: 'creator' }], // This works!
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
console.log(config.creator.name); // Access related User data
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
### Pattern 2: Manual Schema (Full Control)
|
|
398
|
-
|
|
399
|
-
If you need explicit control over the schema, you can still provide it manually:
|
|
234
|
+
If you need explicit control, you can still provide schema manually:
|
|
400
235
|
|
|
401
236
|
```typescript
|
|
402
|
-
import {
|
|
403
|
-
Configuration, configurationTable, configurationRelations,
|
|
404
|
-
User, userTable, userRelations,
|
|
405
|
-
} from '@/models/entities';
|
|
406
|
-
|
|
407
237
|
@datasource({ driver: 'node-postgres' })
|
|
408
238
|
export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
|
|
409
239
|
constructor() {
|
|
410
240
|
super({
|
|
411
241
|
name: PostgresDataSource.name,
|
|
412
242
|
config: { /* ... */ },
|
|
413
|
-
// Manually merge tables and relations using spread syntax
|
|
414
243
|
schema: {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
...configurationRelations.relations,
|
|
244
|
+
User: User.schema,
|
|
245
|
+
Configuration: Configuration.schema,
|
|
246
|
+
// Add relations if using Drizzle's relational queries
|
|
419
247
|
},
|
|
420
248
|
});
|
|
421
249
|
}
|
|
422
|
-
|
|
423
|
-
override configure(): ValueOrPromise<void> {
|
|
424
|
-
this.connector = drizzle({
|
|
425
|
-
client: new Pool(this.settings.connection),
|
|
426
|
-
schema: this.schema,
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
250
|
}
|
|
430
251
|
```
|
|
431
252
|
|
|
432
|
-
### @datasource Decorator
|
|
433
|
-
|
|
434
|
-
```typescript
|
|
435
|
-
@datasource({
|
|
436
|
-
driver: 'node-postgres', // Required - database driver
|
|
437
|
-
autoDiscovery?: true // Optional - defaults to true
|
|
438
|
-
})
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
| Option | Type | Default | Description |
|
|
442
|
-
|--------|------|---------|-------------|
|
|
443
|
-
| `driver` | `TDataSourceDriver` | - | Database driver name |
|
|
444
|
-
| `autoDiscovery` | `boolean` | `true` | Enable/disable schema auto-discovery |
|
|
445
|
-
|
|
446
253
|
### Registering a DataSource
|
|
447
254
|
|
|
448
|
-
Finally, register your `DataSource` with the application in `src/application.ts`.
|
|
449
|
-
|
|
450
255
|
```typescript
|
|
451
256
|
// src/application.ts
|
|
452
|
-
import { PostgresDataSource } from './datasources';
|
|
453
|
-
|
|
454
257
|
export class Application extends BaseApplication {
|
|
455
|
-
// ...
|
|
456
258
|
preConfigure(): ValueOrPromise<void> {
|
|
457
259
|
this.dataSource(PostgresDataSource);
|
|
458
|
-
// ...
|
|
459
260
|
}
|
|
460
261
|
}
|
|
461
262
|
```
|
|
462
263
|
|
|
463
|
-
> **Deep Dive:**
|
|
464
|
-
> - Explore the [**`BaseDataSource`**](../../references/base/datasources.md) class.
|
|
465
|
-
|
|
466
264
|
---
|
|
467
265
|
|
|
468
266
|
## 3. Repositories: The Data Access Layer
|
|
469
267
|
|
|
470
|
-
Repositories
|
|
471
|
-
|
|
472
|
-
### Creating a Repository
|
|
473
|
-
|
|
474
|
-
A repository extends `DefaultCRUDRepository` (for full read/write operations) and is decorated with `@repository`.
|
|
475
|
-
|
|
476
|
-
**IMPORTANT:** Both `model` AND `dataSource` are required in `@repository` for schema auto-discovery. Without both, the model won't be registered and relational queries will fail.
|
|
268
|
+
Repositories provide type-safe CRUD operations. Use `@repository` decorator with both `model` and `dataSource` for auto-discovery.
|
|
477
269
|
|
|
478
|
-
|
|
270
|
+
### Pattern 1: Zero Boilerplate (Recommended)
|
|
479
271
|
|
|
480
|
-
The simplest approach -
|
|
272
|
+
The simplest approach - everything is auto-resolved:
|
|
481
273
|
|
|
482
274
|
```typescript
|
|
483
275
|
// src/repositories/configuration.repository.ts
|
|
484
|
-
import { Configuration
|
|
485
|
-
import { PostgresDataSource } from '@/datasources';
|
|
486
|
-
import {
|
|
276
|
+
import { Configuration } from '@/models/entities';
|
|
277
|
+
import { PostgresDataSource } from '@/datasources/postgres.datasource';
|
|
278
|
+
import { DefaultCRUDRepository, repository } from '@venizia/ignis';
|
|
487
279
|
|
|
488
|
-
@repository({
|
|
489
|
-
|
|
490
|
-
|
|
280
|
+
@repository({
|
|
281
|
+
model: Configuration,
|
|
282
|
+
dataSource: PostgresDataSource,
|
|
283
|
+
})
|
|
284
|
+
export class ConfigurationRepository extends DefaultCRUDRepository<typeof Configuration.schema> {
|
|
285
|
+
// No constructor needed!
|
|
491
286
|
|
|
492
|
-
// Add custom methods as needed
|
|
493
287
|
async findByCode(code: string) {
|
|
494
288
|
return this.findOne({ filter: { where: { code } } });
|
|
495
289
|
}
|
|
290
|
+
|
|
291
|
+
async findByGroup(group: string) {
|
|
292
|
+
return this.find({ filter: { where: { group } } });
|
|
293
|
+
}
|
|
496
294
|
}
|
|
497
295
|
```
|
|
498
296
|
|
|
499
|
-
|
|
297
|
+
### Pattern 2: Explicit @inject
|
|
500
298
|
|
|
501
|
-
When you need constructor control (e.g.,
|
|
299
|
+
When you need constructor control (e.g., read-only repository or additional dependencies):
|
|
502
300
|
|
|
503
301
|
```typescript
|
|
504
302
|
// src/repositories/user.repository.ts
|
|
505
303
|
import { User } from '@/models/entities';
|
|
506
|
-
import { PostgresDataSource } from '@/datasources';
|
|
507
|
-
import { inject,
|
|
304
|
+
import { PostgresDataSource } from '@/datasources/postgres.datasource';
|
|
305
|
+
import { inject, ReadableRepository, repository } from '@venizia/ignis';
|
|
306
|
+
import { CacheService } from '@/services/cache.service';
|
|
508
307
|
|
|
509
308
|
@repository({ model: User, dataSource: PostgresDataSource })
|
|
510
309
|
export class UserRepository extends ReadableRepository<typeof User.schema> {
|
|
511
310
|
constructor(
|
|
311
|
+
// First parameter MUST be DataSource injection
|
|
512
312
|
@inject({ key: 'datasources.PostgresDataSource' })
|
|
513
|
-
dataSource: PostgresDataSource,
|
|
313
|
+
dataSource: PostgresDataSource, // Must be concrete type, not 'any'
|
|
314
|
+
|
|
315
|
+
// After first arg, you can inject any additional dependencies
|
|
316
|
+
@inject({ key: 'some.cache' })
|
|
317
|
+
private cache: SomeCache,
|
|
514
318
|
) {
|
|
515
319
|
super(dataSource);
|
|
516
320
|
}
|
|
517
321
|
|
|
518
322
|
async findByRealm(realm: string) {
|
|
323
|
+
// Use injected dependencies
|
|
324
|
+
const cached = await this.cacheService.get(`user:realm:${realm}`);
|
|
325
|
+
if (cached) {
|
|
326
|
+
return cached;
|
|
327
|
+
}
|
|
328
|
+
|
|
519
329
|
return this.findOne({ filter: { where: { realm } } });
|
|
520
330
|
}
|
|
521
331
|
}
|
|
522
332
|
```
|
|
523
333
|
|
|
524
|
-
**
|
|
334
|
+
> **Important:**
|
|
335
|
+
> - First constructor parameter **MUST** be the DataSource injection
|
|
336
|
+
> - After the first argument, you can inject any additional dependencies you need
|
|
337
|
+
> - When `@inject` is at param index 0, auto-injection is skipped
|
|
525
338
|
|
|
526
|
-
|
|
339
|
+
### Repository Types
|
|
527
340
|
|
|
528
|
-
|
|
341
|
+
| Type | Description |
|
|
342
|
+
|------|-------------|
|
|
343
|
+
| `DefaultCRUDRepository` | Full read/write operations |
|
|
344
|
+
| `ReadableRepository` | Read-only operations |
|
|
345
|
+
| `PersistableRepository` | Write operations only |
|
|
529
346
|
|
|
530
|
-
|
|
347
|
+
### Querying Data
|
|
531
348
|
|
|
532
349
|
```typescript
|
|
533
|
-
// Example usage in application.ts or a service
|
|
534
350
|
const repo = this.get<ConfigurationRepository>({ key: 'repositories.ConfigurationRepository' });
|
|
535
351
|
|
|
536
352
|
// Find multiple records
|
|
537
|
-
const
|
|
353
|
+
const configs = await repo.find({
|
|
538
354
|
filter: {
|
|
539
355
|
where: { group: 'SYSTEM' },
|
|
540
356
|
limit: 10,
|
|
@@ -542,30 +358,186 @@ const someConfigs = await repo.find({
|
|
|
542
358
|
}
|
|
543
359
|
});
|
|
544
360
|
|
|
545
|
-
//
|
|
361
|
+
// Find one record
|
|
362
|
+
const config = await repo.findOne({
|
|
363
|
+
filter: { where: { code: 'APP_NAME' } }
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Create a record
|
|
546
367
|
const newConfig = await repo.create({
|
|
547
368
|
data: {
|
|
548
|
-
code: '
|
|
369
|
+
code: 'NEW_SETTING',
|
|
549
370
|
group: 'SYSTEM',
|
|
550
|
-
|
|
371
|
+
description: 'A new setting',
|
|
551
372
|
}
|
|
552
373
|
});
|
|
374
|
+
|
|
375
|
+
// Update by ID
|
|
376
|
+
await repo.updateById({
|
|
377
|
+
id: 'uuid-here',
|
|
378
|
+
data: { description: 'Updated description' }
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Delete by ID
|
|
382
|
+
await repo.deleteById({ id: 'uuid-here' });
|
|
553
383
|
```
|
|
554
384
|
|
|
555
385
|
### Querying with Relations
|
|
556
386
|
|
|
557
|
-
|
|
387
|
+
Use `include` to fetch related data. The relation name must match what you defined in `static relations`:
|
|
558
388
|
|
|
559
389
|
```typescript
|
|
560
|
-
const
|
|
390
|
+
const configWithCreator = await repo.findOne({
|
|
561
391
|
filter: {
|
|
562
|
-
where: { code: '
|
|
563
|
-
include: [{ relation: 'creator' }],
|
|
392
|
+
where: { code: 'APP_NAME' },
|
|
393
|
+
include: [{ relation: 'creator' }],
|
|
564
394
|
},
|
|
565
395
|
});
|
|
566
396
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
397
|
+
console.log('Created by:', configWithCreator.creator.name);
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Registering Repositories
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// src/application.ts
|
|
404
|
+
export class Application extends BaseApplication {
|
|
405
|
+
preConfigure(): ValueOrPromise<void> {
|
|
406
|
+
this.dataSource(PostgresDataSource);
|
|
407
|
+
this.repository(UserRepository);
|
|
408
|
+
this.repository(ConfigurationRepository);
|
|
409
|
+
}
|
|
570
410
|
}
|
|
571
411
|
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## 4. Advanced Topics
|
|
416
|
+
|
|
417
|
+
### Performance: Core API Optimization
|
|
418
|
+
|
|
419
|
+
Ignis automatically optimizes "flat" queries (no relations, no field selection) by using Drizzle's Core API. This provides **~15-20% faster** queries for simple reads.
|
|
420
|
+
|
|
421
|
+
### Transactions (Current)
|
|
422
|
+
|
|
423
|
+
Currently, use Drizzle's callback-based `connector.transaction` for atomic operations:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
const ds = this.get<PostgresDataSource>({ key: 'datasources.PostgresDataSource' });
|
|
427
|
+
|
|
428
|
+
await ds.connector.transaction(async (tx) => {
|
|
429
|
+
await tx.insert(User.schema).values({ /* ... */ });
|
|
430
|
+
await tx.insert(Configuration.schema).values({ /* ... */ });
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
> **Note:** This callback-based approach requires all transaction logic to be in one callback. See [Section 5](#5-transactions-planned) for the planned improvement.
|
|
435
|
+
|
|
436
|
+
### Modular Persistence with Components
|
|
437
|
+
|
|
438
|
+
Bundle related persistence resources into Components for better organization:
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
export class UserManagementComponent extends BaseComponent {
|
|
442
|
+
override binding() {
|
|
443
|
+
this.application.dataSource(PostgresDataSource);
|
|
444
|
+
this.application.repository(UserRepository);
|
|
445
|
+
this.application.repository(ProfileRepository);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## 5. Transactions (Planned)
|
|
453
|
+
|
|
454
|
+
> **Status:** Planned - Not yet implemented. See [full plan](../../changelogs/planned-transaction-support).
|
|
455
|
+
|
|
456
|
+
### The Problem
|
|
457
|
+
|
|
458
|
+
Drizzle's callback-based transactions make it hard to pass transactions across services:
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// Current: Everything must be inside the callback
|
|
462
|
+
await ds.connector.transaction(async (tx) => {
|
|
463
|
+
// Can't easily call other services with this tx
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Planned Solution
|
|
468
|
+
|
|
469
|
+
Loopback 4-style explicit transaction objects that can be passed anywhere:
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
// Start transaction from repository
|
|
473
|
+
const tx = await userRepo.beginTransaction({
|
|
474
|
+
isolationLevel: 'SERIALIZABLE' // Optional, defaults to 'READ COMMITTED'
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
// Pass tx to multiple services/repositories
|
|
479
|
+
const user = await userRepo.create({ data, options: { transaction: tx } });
|
|
480
|
+
await profileRepo.create({ data: { userId: user.id }, options: { transaction: tx } });
|
|
481
|
+
await orderService.createInitialOrder(user.id, { transaction: tx });
|
|
482
|
+
|
|
483
|
+
await tx.commit();
|
|
484
|
+
} catch (err) {
|
|
485
|
+
await tx.rollback();
|
|
486
|
+
throw err;
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Isolation Levels
|
|
491
|
+
|
|
492
|
+
| Level | Description | Use Case |
|
|
493
|
+
|-------|-------------|----------|
|
|
494
|
+
| `READ COMMITTED` | Default. Sees only committed data | General use |
|
|
495
|
+
| `REPEATABLE READ` | Snapshot from transaction start | Reports, consistent reads |
|
|
496
|
+
| `SERIALIZABLE` | Full isolation, may throw errors | Financial, critical data |
|
|
497
|
+
|
|
498
|
+
### Benefits
|
|
499
|
+
|
|
500
|
+
| Aspect | Current (Callback) | Planned (Pass-through) |
|
|
501
|
+
|--------|-------------------|------------------------|
|
|
502
|
+
| Service composition | Hard | Easy - pass tx anywhere |
|
|
503
|
+
| Separation of concerns | Services coupled | Services independent |
|
|
504
|
+
| Testing | Complex mocking | Easy to mock tx |
|
|
505
|
+
| Code organization | Nested callbacks | Flat, sequential |
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Quick Reference
|
|
510
|
+
|
|
511
|
+
### Model Template
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
import { BaseEntity, generateIdColumnDefs, model, TRelationConfig } from '@venizia/ignis';
|
|
515
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
516
|
+
|
|
517
|
+
@model({ type: 'entity' })
|
|
518
|
+
export class MyModel extends BaseEntity<typeof MyModel.schema> {
|
|
519
|
+
static override schema = pgTable('MyModel', {
|
|
520
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
521
|
+
name: text('name').notNull(),
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
static override relations = (): TRelationConfig[] => [];
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Repository Template
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
import { DefaultCRUDRepository, repository } from '@venizia/ignis';
|
|
532
|
+
import { MyModel } from '@/models/entities';
|
|
533
|
+
import { PostgresDataSource } from '@/datasources/postgres.datasource';
|
|
534
|
+
|
|
535
|
+
@repository({ model: MyModel, dataSource: PostgresDataSource })
|
|
536
|
+
export class MyModelRepository extends DefaultCRUDRepository<typeof MyModel.schema> {}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
> **Deep Dive:**
|
|
540
|
+
> - [BaseEntity Reference](../../references/base/models.md#baseentity-class)
|
|
541
|
+
> - [Schema Enrichers](../../references/base/models.md#schema-enrichers)
|
|
542
|
+
> - [BaseDataSource Reference](../../references/base/datasources.md)
|
|
543
|
+
> - [Repository Reference](../../references/base/repositories.md)
|