@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.
Files changed (34) hide show
  1. package/LICENSE.md +1 -0
  2. package/package.json +2 -2
  3. package/wiki/changelogs/{v0.0.1-7-initial-architecture.md → 2025-12-16-initial-architecture.md} +20 -12
  4. package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +300 -0
  5. package/wiki/changelogs/2025-12-17-refactor.md +80 -12
  6. package/wiki/changelogs/2025-12-18-performance-optimizations.md +28 -90
  7. package/wiki/changelogs/2025-12-18-repository-validation-security.md +101 -297
  8. package/wiki/changelogs/index.md +20 -8
  9. package/wiki/changelogs/planned-schema-migrator.md +561 -0
  10. package/wiki/changelogs/planned-transaction-support.md +216 -0
  11. package/wiki/changelogs/template.md +123 -0
  12. package/wiki/get-started/best-practices/api-usage-examples.md +0 -2
  13. package/wiki/get-started/best-practices/architectural-patterns.md +2 -2
  14. package/wiki/get-started/best-practices/code-style-standards.md +575 -10
  15. package/wiki/get-started/best-practices/common-pitfalls.md +5 -3
  16. package/wiki/get-started/best-practices/contribution-workflow.md +2 -0
  17. package/wiki/get-started/best-practices/data-modeling.md +91 -34
  18. package/wiki/get-started/best-practices/security-guidelines.md +3 -1
  19. package/wiki/get-started/building-a-crud-api.md +3 -3
  20. package/wiki/get-started/core-concepts/application.md +72 -3
  21. package/wiki/get-started/core-concepts/bootstrapping.md +566 -0
  22. package/wiki/get-started/core-concepts/components.md +4 -2
  23. package/wiki/get-started/core-concepts/persistent.md +350 -378
  24. package/wiki/get-started/core-concepts/services.md +21 -27
  25. package/wiki/references/base/bootstrapping.md +789 -0
  26. package/wiki/references/base/components.md +1 -1
  27. package/wiki/references/base/dependency-injection.md +95 -2
  28. package/wiki/references/base/services.md +2 -2
  29. package/wiki/references/components/authentication.md +4 -3
  30. package/wiki/references/components/index.md +1 -1
  31. package/wiki/references/helpers/error.md +2 -2
  32. package/wiki/references/src-details/boot.md +379 -0
  33. package/wiki/references/src-details/core.md +2 -2
  34. 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
- - **Models** - Define data structure (Drizzle schemas + Entity classes)
7
- - **DataSources** - Manage database connections
8
- - **Repositories** - Provide CRUD operations
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 `Ignis` consists of two parts: a **Drizzle schema** that defines the database table and an **Entity class** that wraps it for use within the framework.
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
- static override schema = userTable;
50
- static override relations = () => userRelations.definitions;
51
- static override TABLE_NAME = 'User';
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
- **What this gives you:**
167
- ```typescript
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
- #### When NOT to Use Enrichers
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
- **Key Concepts:**
203
- - **`pgTable`**: The standard function from Drizzle ORM to define a table schema.
204
- - **Enrichers**: Ignis provides helper functions like `generateIdColumnDefs()` and `extraUserColumns()` that add common, pre-configured columns (like `id`, `status`, `type`, etc.) to your schema, reducing boilerplate.
205
- - **`createRelations`**: A helper for defining relationships between models. Even if there are no relations, you must call it.
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
- TTableObject,
59
+ TRelationConfig,
225
60
  } from '@venizia/ignis';
226
61
  import { foreignKey, index, pgTable, text, unique } from 'drizzle-orm/pg-core';
227
- import { User, userTable } from './user.model';
62
+ import { User } from './user.model';
228
63
 
229
- // 1. Define the Drizzle schema for the 'Configuration' table
230
- export const configurationTable = pgTable(
231
- Configuration.name,
232
- {
233
- ...generateIdColumnDefs({ id: { dataType: 'string' } }),
234
- ...generateTzColumnDefs(),
235
- ...generateDataTypeColumnDefs(),
236
- ...generateUserAuditColumnDefs({
237
- created: { dataType: 'string', columnName: 'created_by' },
238
- modified: { dataType: 'string', columnName: 'modified_by' },
239
- }),
240
- code: text('code').notNull(),
241
- description: text('description'),
242
- group: text('group').notNull(),
243
- },
244
- (def) => [
245
- unique(`UQ_${Configuration.name}_code`).on(def.code),
246
- index(`IDX_${Configuration.name}_group`).on(def.group),
247
- foreignKey({
248
- columns: [def.createdBy],
249
- foreignColumns: [userTable.id],
250
- name: `FK_${Configuration.name}_createdBy_${User.name}_id`,
251
- }),
252
- ],
253
- );
254
-
255
- // 2. Define the relations using Ignis's `createRelations` helper
256
- export const configurationRelations = createRelations({
257
- source: configurationTable,
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: userTable,
96
+ schema: User.schema,
263
97
  metadata: {
264
- fields: [configurationTable.createdBy],
265
- references: [userTable.id],
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: userTable,
105
+ schema: User.schema,
272
106
  metadata: {
273
- fields: [configurationTable.modifiedBy],
274
- references: [userTable.id],
107
+ fields: [Configuration.schema.modifiedBy],
108
+ references: [User.schema.id],
275
109
  },
276
110
  },
277
- ],
278
- });
111
+ ];
112
+ }
113
+ ```
279
114
 
280
- // 3. Define types and the Entity class as before
281
- export type TConfigurationSchema = typeof configurationTable;
282
- export type TConfiguration = TTableObject<TConfigurationSchema>;
115
+ **Key points:**
283
116
 
284
- @model({ type: 'entity' })
285
- export class Configuration extends BaseEntity<typeof Configuration.schema> {
286
- static override schema = configurationTable;
287
- static override relations = () => configurationRelations.definitions;
288
- static override TABLE_NAME = 'Configuration';
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
- > **Deep Dive:**
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
- ## 2. DataSources: Connecting to Your Database
149
+ #### Available Enrichers
301
150
 
302
- A DataSource is a class responsible for managing the connection to your database and making the Drizzle ORM instance available to your application.
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
- ### Creating and Configuring a DataSource
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
- A `DataSource` must be decorated with `@datasource`. The framework now supports **schema auto-discovery**, which means you no longer need to manually merge tables and relations!
165
+ ## 2. DataSources: Connecting to Your Database
307
166
 
308
- ### Pattern 1: Auto-Discovery (Recommended)
167
+ A DataSource manages database connections and supports **schema auto-discovery** from repositories.
309
168
 
310
- With auto-discovery, the schema is automatically built from your `@repository` decorators:
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
- connection: {
325
- host?: string;
326
- port?: number;
327
- user?: string;
328
- password?: string;
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
- connection: {
340
- host: process.env.APP_ENV_POSTGRES_HOST,
341
- port: +(process.env.APP_ENV_POSTGRES_PORT ?? 5432),
342
- user: process.env.APP_ENV_POSTGRES_USERNAME,
343
- password: process.env.APP_ENV_POSTGRES_PASSWORD,
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 decorators!
202
+ // No schema needed - auto-discovered from @repository bindings!
348
203
  });
349
204
  }
350
205
 
351
206
  override configure(): ValueOrPromise<void> {
352
- this.connector = drizzle({
353
- client: new Pool(this.settings.connection),
354
- schema: this.schema, // Auto-discovered schema
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
- override async connect(): Promise<TNodePostgresConnector | undefined> {
359
- await (this.connector.client as Pool).connect();
360
- return this.connector;
215
+ const client = new Pool(this.settings);
216
+ this.connector = drizzle({ client, schema });
361
217
  }
362
218
 
363
- override async disconnect(): Promise<void> {
364
- await (this.connector.client as Pool).end();
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
- When you define repositories with both `model` and `dataSource`:
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
- ```typescript
374
- @repository({ model: User, dataSource: PostgresDataSource })
375
- export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
232
+ ### Manual Schema (Optional)
376
233
 
377
- @repository({ model: Configuration, dataSource: PostgresDataSource })
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
- [User.TABLE_NAME]: userTable,
416
- [Configuration.TABLE_NAME]: configurationTable,
417
- ...userRelations.relations,
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 abstract the data access logic. They use the configured `DataSource` to perform type-safe queries against the database.
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
- #### Pattern 1: Zero Boilerplate (Recommended)
270
+ ### Pattern 1: Zero Boilerplate (Recommended)
479
271
 
480
- The simplest approach - dataSource is auto-injected from metadata:
272
+ The simplest approach - everything is auto-resolved:
481
273
 
482
274
  ```typescript
483
275
  // src/repositories/configuration.repository.ts
484
- import { Configuration, TConfigurationSchema } from '@/models/entities';
485
- import { PostgresDataSource } from '@/datasources';
486
- import { repository, DefaultCRUDRepository } from '@venizia/ignis';
276
+ import { Configuration } from '@/models/entities';
277
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
278
+ import { DefaultCRUDRepository, repository } from '@venizia/ignis';
487
279
 
488
- @repository({ model: Configuration, dataSource: PostgresDataSource })
489
- export class ConfigurationRepository extends DefaultCRUDRepository<TConfigurationSchema> {
490
- // No constructor needed - datasource auto-injected!
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
- #### Pattern 2: Explicit @inject
297
+ ### Pattern 2: Explicit @inject
500
298
 
501
- When you need constructor control (e.g., for read-only repositories or custom initialization):
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, repository, ReadableRepository } from '@venizia/ignis';
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, // Must be concrete DataSource type, NOT 'any'
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
- **Note:** When `@inject` is at param index 0, auto-injection is skipped (your `@inject` takes precedence).
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
- You would then register this repository in your `application.ts`: `this.repository(ConfigurationRepository);`
339
+ ### Repository Types
527
340
 
528
- ### Querying Data
341
+ | Type | Description |
342
+ |------|-------------|
343
+ | `DefaultCRUDRepository` | Full read/write operations |
344
+ | `ReadableRepository` | Read-only operations |
345
+ | `PersistableRepository` | Write operations only |
529
346
 
530
- Repositories provide a full suite of type-safe methods for CRUD operations using a standardized `filter` object.
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 someConfigs = await repo.find({
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
- // Create a new record
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: 'NEW_CODE',
369
+ code: 'NEW_SETTING',
549
370
  group: 'SYSTEM',
550
- // ... other fields
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
- To query related data, use the `include` property in the filter object. The `relation` name must match one of the names you defined in `createRelations` (e.g., `creator`).
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 resultsWithCreator = await repo.find({
390
+ const configWithCreator = await repo.findOne({
561
391
  filter: {
562
- where: { code: 'some_code' },
563
- include: [{ relation: 'creator' }], // Fetch the related user
392
+ where: { code: 'APP_NAME' },
393
+ include: [{ relation: 'creator' }],
564
394
  },
565
395
  });
566
396
 
567
- if (resultsWithCreator.length > 0) {
568
- // `resultsWithCreator[0].creator` will contain the full User object
569
- console.log('Configuration created by:', resultsWithCreator[0].creator.name);
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)