@venizia/ignis-docs 0.0.1-8 → 0.0.1

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 (43) hide show
  1. package/LICENSE.md +1 -0
  2. package/package.json +2 -2
  3. package/wiki/changelogs/2025-12-16-initial-architecture.md +145 -0
  4. package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +300 -0
  5. package/wiki/changelogs/2025-12-17-refactor.md +90 -0
  6. package/wiki/changelogs/2025-12-18-performance-optimizations.md +130 -0
  7. package/wiki/changelogs/2025-12-18-repository-validation-security.md +249 -0
  8. package/wiki/changelogs/index.md +33 -0
  9. package/wiki/changelogs/planned-transaction-support.md +216 -0
  10. package/wiki/changelogs/template.md +123 -0
  11. package/wiki/get-started/5-minute-quickstart.md +1 -1
  12. package/wiki/get-started/best-practices/api-usage-examples.md +12 -10
  13. package/wiki/get-started/best-practices/architectural-patterns.md +2 -2
  14. package/wiki/get-started/best-practices/common-pitfalls.md +7 -5
  15. package/wiki/get-started/best-practices/contribution-workflow.md +2 -0
  16. package/wiki/get-started/best-practices/data-modeling.md +91 -40
  17. package/wiki/get-started/best-practices/security-guidelines.md +3 -1
  18. package/wiki/get-started/building-a-crud-api.md +63 -78
  19. package/wiki/get-started/core-concepts/application.md +72 -3
  20. package/wiki/get-started/core-concepts/bootstrapping.md +566 -0
  21. package/wiki/get-started/core-concepts/components.md +4 -2
  22. package/wiki/get-started/core-concepts/controllers.md +14 -14
  23. package/wiki/get-started/core-concepts/persistent.md +383 -431
  24. package/wiki/get-started/core-concepts/services.md +21 -27
  25. package/wiki/get-started/quickstart.md +1 -1
  26. package/wiki/references/base/bootstrapping.md +789 -0
  27. package/wiki/references/base/components.md +1 -1
  28. package/wiki/references/base/controllers.md +40 -16
  29. package/wiki/references/base/datasources.md +195 -33
  30. package/wiki/references/base/dependency-injection.md +98 -5
  31. package/wiki/references/base/models.md +398 -28
  32. package/wiki/references/base/repositories.md +475 -22
  33. package/wiki/references/base/services.md +2 -2
  34. package/wiki/references/components/authentication.md +228 -10
  35. package/wiki/references/components/health-check.md +1 -1
  36. package/wiki/references/components/index.md +1 -1
  37. package/wiki/references/components/swagger.md +1 -1
  38. package/wiki/references/helpers/error.md +2 -2
  39. package/wiki/references/helpers/inversion.md +8 -3
  40. package/wiki/references/src-details/boot.md +379 -0
  41. package/wiki/references/src-details/core.md +8 -7
  42. package/wiki/references/src-details/inversion.md +4 -4
  43. package/wiki/references/utilities/request.md +16 -7
@@ -1,342 +1,175 @@
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
- export class User extends BaseEntity<TUserSchema> {
49
- static readonly TABLE_NAME = User.name;
50
-
51
- constructor() {
52
- super({ name: User.name, schema: userTable });
53
- }
54
- }
55
- ```
56
-
57
- ### Understanding Enrichers: The Smart Column Generators
58
-
59
- 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.
60
-
61
- #### Why Enrichers Exist
62
-
63
- **Without enrichers (the hard way):**
64
- ```typescript
65
- export const userTable = pgTable('User', {
66
- // Manually define every common column in every table
67
- id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
68
- status: text('status').notNull().default('ACTIVE'),
69
- type: text('type'),
70
- createdBy: text('created_by'),
71
- modifiedBy: text('modified_by'),
72
- createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
73
- modifiedAt: timestamp('modified_at', { withTimezone: true }).notNull().defaultNow(),
74
- // ... your actual user-specific fields
75
- email: text('email').notNull(),
76
- name: text('name'),
77
- });
78
- ```
79
-
80
- **With enrichers (the smart way):**
81
- ```typescript
82
- export const userTable = pgTable('User', {
83
- ...generateIdColumnDefs({ id: { dataType: 'string' } }), // Adds: id (UUID)
84
- ...extraUserColumns({ idType: 'string' }), // Adds: status, type, createdBy, modifiedBy, createdAt, modifiedAt
85
- // Just your actual user-specific fields
86
- email: text('email').notNull(),
87
- name: text('name'),
88
- });
89
- ```
90
-
91
- **Result:** Same table structure, but with:
92
- - 7 fewer lines of code
93
- - Guaranteed consistency across all tables
94
- - Less chance of typos or mistakes
95
- - Easier to maintain
96
-
97
- #### Common Enrichers
98
-
99
- | Enricher | What It Adds | Use Case |
100
- | :--- | :--- | :--- |
101
- | `generateIdColumnDefs()` | Primary key `id` column (UUID or number) | Every table needs an ID |
102
- | `generateTzColumnDefs()` | `createdAt` and `modifiedAt` timestamps | Track when records are created/updated |
103
- | `generateUserAuditColumnDefs()` | `createdBy` and `modifiedBy` foreign keys | Track which user created/updated records |
104
- | `generateDataTypeColumnDefs()` | `dataType` and type-specific value columns (`tValue`, `nValue`, etc.) | Configuration tables with mixed data types |
105
- | `extraUserColumns()` | Combination of audit + status + type fields | Full-featured entity tables |
106
-
107
- #### Practical Example: Building a Post Model
108
-
109
- Let's create a blog post model using enrichers:
110
-
111
- ```typescript
112
- // src/models/post.model.ts
113
- import {
114
- BaseEntity,
115
- createRelations,
116
- generateIdColumnDefs,
117
- generateTzColumnDefs,
118
- generateUserAuditColumnDefs,
119
- model,
120
- RelationTypes,
121
- TTableObject,
122
- } from '@venizia/ignis';
123
- import { pgTable, text, boolean } from 'drizzle-orm/pg-core';
124
- import { userTable } from './user.model';
125
-
126
- export const postTable = pgTable('Post', {
127
- // Use enrichers for common columns
128
- ...generateIdColumnDefs({ id: { dataType: 'string' } }), // id: UUID primary key
129
- ...generateTzColumnDefs(), // createdAt, modifiedAt
130
- ...generateUserAuditColumnDefs({ // createdBy, modifiedBy
131
- created: { dataType: 'string', columnName: 'created_by' },
132
- modified: { dataType: 'string', columnName: 'modified_by' },
133
- }),
134
-
135
- // Your post-specific fields
136
- title: text('title').notNull(),
137
- content: text('content').notNull(),
138
- isPublished: boolean('is_published').default(false),
139
- slug: text('slug').notNull().unique(),
140
- });
141
-
142
- export const postRelations = createRelations({
143
- source: postTable,
144
- relations: [
145
- {
146
- name: 'author',
147
- type: RelationTypes.ONE,
148
- schema: userTable,
149
- metadata: {
150
- fields: [postTable.createdBy],
151
- references: [userTable.id],
152
- },
153
- },
154
- ],
155
- });
156
-
157
- export type TPostSchema = typeof postTable;
158
- export type TPost = TTableObject<TPostSchema>;
159
-
160
- @model({ type: 'entity' })
161
- export class Post extends BaseEntity<TPostSchema> {
162
- static readonly TABLE_NAME = 'Post';
163
-
164
- constructor() {
165
- super({ name: Post.TABLE_NAME, schema: postTable });
166
- }
167
- }
168
- ```
28
+ export class User extends BaseEntity<typeof User.schema> {
29
+ // Define schema as static property
30
+ static override schema = pgTable('User', {
31
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
32
+ ...extraUserColumns({ idType: 'string' }),
33
+ });
169
34
 
170
- **What this gives you:**
171
- ```typescript
172
- interface Post {
173
- id: string; // From generateIdColumnDefs
174
- createdAt: Date; // From generateTzColumnDefs
175
- modifiedAt: Date; // From generateTzColumnDefs
176
- createdBy: string; // From generateUserAuditColumnDefs
177
- modifiedBy: string; // From generateUserAuditColumnDefs
178
- title: string; // Your field
179
- content: string; // Your field
180
- isPublished: boolean; // Your field
181
- slug: string; // Your field
35
+ // Relations (empty array if none)
36
+ static override relations = () => [];
182
37
  }
183
38
  ```
184
39
 
185
- #### When NOT to Use Enrichers
186
-
187
- You can always define columns manually if:
188
- - You need a custom ID strategy (e.g., integer auto-increment)
189
- - You don't need audit fields for a specific table
190
- - You have very specific timestamp requirements
191
-
192
- ```typescript
193
- // Mixing enrichers with manual columns is perfectly fine
194
- export const simpleTable = pgTable('Simple', {
195
- ...generateIdColumnDefs({ id: { dataType: 'number' } }), // Use enricher for ID
196
- // But manually define everything else
197
- name: text('name').notNull(),
198
- value: integer('value'),
199
- });
200
- ```
201
-
202
- :::tip
203
- For a complete list of available enrichers and their options, see the [**Schema Enrichers Reference**](../../references/base/models.md#schema-enrichers).
204
- :::
40
+ **Key points:**
205
41
 
206
- **Key Concepts:**
207
- - **`pgTable`**: The standard function from Drizzle ORM to define a table schema.
208
- - **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.
209
- - **`createRelations`**: A helper for defining relationships between models. Even if there are no relations, you must call it.
210
- - **`BaseEntity`**: The class your model extends. It wraps the Drizzle schema and provides utilities for the framework.
211
- - **`@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)
212
46
 
213
47
  ### Creating a Model with Relations
214
48
 
215
- Now, let's create a `Configuration` model that has a relationship with the `User` model.
216
-
217
49
  ```typescript
218
50
  // src/models/entities/configuration.model.ts
219
51
  import {
220
52
  BaseEntity,
221
- createRelations, // Import createRelations
222
53
  generateDataTypeColumnDefs,
223
54
  generateIdColumnDefs,
224
55
  generateTzColumnDefs,
225
56
  generateUserAuditColumnDefs,
226
57
  model,
227
58
  RelationTypes,
228
- TTableObject,
59
+ TRelationConfig,
229
60
  } from '@venizia/ignis';
230
61
  import { foreignKey, index, pgTable, text, unique } from 'drizzle-orm/pg-core';
231
- import { User, userTable } from './user.model';
62
+ import { User } from './user.model';
232
63
 
233
- // 1. Define the Drizzle schema for the 'Configuration' table
234
- export const configurationTable = pgTable(
235
- Configuration.name,
236
- {
237
- ...generateIdColumnDefs({ id: { dataType: 'string' } }),
238
- ...generateTzColumnDefs(),
239
- ...generateDataTypeColumnDefs(),
240
- ...generateUserAuditColumnDefs({
241
- created: { dataType: 'string', columnName: 'created_by' },
242
- modified: { dataType: 'string', columnName: 'modified_by' },
243
- }),
244
- code: text('code').notNull(),
245
- description: text('description'),
246
- group: text('group').notNull(),
247
- },
248
- (def) => [
249
- unique(`UQ_${Configuration.name}_code`).on(def.code),
250
- index(`IDX_${Configuration.name}_group`).on(def.group),
251
- foreignKey({
252
- columns: [def.createdBy],
253
- foreignColumns: [userTable.id],
254
- name: `FK_${Configuration.name}_createdBy_${User.name}_id`,
255
- }),
256
- ],
257
- );
258
-
259
- // 2. Define the relations using Ignis's `createRelations` helper
260
- export const configurationRelations = createRelations({
261
- source: configurationTable,
262
- 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[] => [
263
93
  {
264
94
  name: 'creator',
265
95
  type: RelationTypes.ONE,
266
- schema: userTable,
96
+ schema: User.schema,
267
97
  metadata: {
268
- fields: [configurationTable.createdBy],
269
- references: [userTable.id],
98
+ fields: [Configuration.schema.createdBy],
99
+ references: [User.schema.id],
270
100
  },
271
101
  },
272
102
  {
273
103
  name: 'modifier',
274
104
  type: RelationTypes.ONE,
275
- schema: userTable,
105
+ schema: User.schema,
276
106
  metadata: {
277
- fields: [configurationTable.modifiedBy],
278
- references: [userTable.id],
107
+ fields: [Configuration.schema.modifiedBy],
108
+ references: [User.schema.id],
279
109
  },
280
110
  },
281
- ],
282
- });
111
+ ];
112
+ }
113
+ ```
283
114
 
284
- // 3. Define types and the Entity class as before
285
- export type TConfigurationSchema = typeof configurationTable;
286
- export type TConfiguration = TTableObject<TConfigurationSchema>;
115
+ **Key points:**
287
116
 
288
- @model({ type: 'entity' })
289
- export class Configuration extends BaseEntity<TConfigurationSchema> {
290
- static readonly TABLE_NAME = Configuration.name;
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`
291
120
 
292
- constructor() {
293
- super({ name: Configuration.TABLE_NAME, schema: configurationTable });
294
- }
295
- }
296
- ```
297
- **Key Concepts:**
298
- - **`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.
121
+ ### Understanding Enrichers
299
122
 
300
- > **Deep Dive:**
301
- > - Explore the [**`BaseEntity`**](../../references/base/models.md#baseentity-class) class.
302
- > - See all available [**Enrichers**](../../references/base/models.md#schema-enrichers) for schema generation.
123
+ Enrichers are helper functions that generate common database columns automatically.
303
124
 
304
- ---
125
+ **Without enrichers:**
305
126
 
306
- ## 2. DataSources: Connecting to Your Database
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
+ });
137
+ ```
307
138
 
308
- A DataSource is a class responsible for managing the connection to your database and making the Drizzle ORM instance available to your application.
139
+ **With enrichers:**
309
140
 
310
- ### Creating and Configuring a DataSource
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
+ ```
311
148
 
312
- A `DataSource` must be decorated with `@datasource`. **The most critical part** is correctly merging your table schemas and relations into a single object that Drizzle ORM can understand.
149
+ #### Available Enrichers
313
150
 
314
- #### ⚠️ Understanding Schema Merging (CRITICAL CONCEPT)
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 |
315
158
 
316
- This is one of the most important concepts in Ignis. If you don't get this right, your relations won't work.
159
+ :::tip
160
+ For a complete list of enrichers and options, see the [Schema Enrichers Reference](../../references/base/models.md#schema-enrichers).
161
+ :::
317
162
 
318
- **The Problem:**
319
- Drizzle ORM needs to know about:
320
- 1. Your table structures (e.g., `userTable`, `configurationTable`)
321
- 2. The relationships between them (e.g., "Configuration belongs to User")
163
+ ---
322
164
 
323
- **The Solution:**
324
- You must merge both into a single `schema` object in your DataSource constructor.
165
+ ## 2. DataSources: Connecting to Your Database
325
166
 
326
- #### Step-by-Step Example
167
+ A DataSource manages database connections and supports **schema auto-discovery** from repositories.
327
168
 
328
- Let's say you have two models: `User` and `Configuration`. Here's how to set up the DataSource:
169
+ ### Creating a DataSource
329
170
 
330
171
  ```typescript
331
172
  // src/datasources/postgres.datasource.ts
332
- import {
333
- Configuration,
334
- configurationTable, // The table structure
335
- configurationRelations, // The relationships
336
- User,
337
- userTable, // The table structure
338
- userRelations, // The relationships
339
- } from '@/models/entities';
340
173
  import {
341
174
  BaseDataSource,
342
175
  datasource,
@@ -346,215 +179,178 @@ import {
346
179
  import { drizzle } from 'drizzle-orm/node-postgres';
347
180
  import { Pool } from 'pg';
348
181
 
349
- // Configuration interface for database connection
350
182
  interface IDSConfigs {
351
- connection: {
352
- host?: string;
353
- port?: number;
354
- user?: string;
355
- password?: string;
356
- database?: string;
357
- };
183
+ host: string;
184
+ port: number;
185
+ database: string;
186
+ user: string;
187
+ password: string;
358
188
  }
359
189
 
360
- @datasource()
190
+ @datasource({ driver: 'node-postgres' })
361
191
  export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
362
192
  constructor() {
363
193
  super({
364
194
  name: PostgresDataSource.name,
365
- driver: 'node-postgres',
366
195
  config: {
367
- connection: {
368
- host: process.env.APP_ENV_POSTGRES_HOST,
369
- port: +(process.env.APP_ENV_POSTGRES_PORT ?? 5432),
370
- user: process.env.APP_ENV_POSTGRES_USERNAME,
371
- password: process.env.APP_ENV_POSTGRES_PASSWORD,
372
- database: process.env.APP_ENV_POSTGRES_DATABASE,
373
- },
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 ?? '',
374
201
  },
375
-
376
- // 🔥 CRITICAL: This is where you merge tables and relations
377
- schema: Object.assign(
378
- {},
379
- // Step 1: Add your table schemas
380
- {
381
- [User.TABLE_NAME]: userTable,
382
- [Configuration.TABLE_NAME]: configurationTable,
383
- },
384
- // Step 2: Spread the relations objects
385
- userRelations.relations,
386
- configurationRelations.relations,
387
- ),
202
+ // No schema needed - auto-discovered from @repository bindings!
388
203
  });
389
204
  }
390
205
 
391
206
  override configure(): ValueOrPromise<void> {
392
- // Pass the merged schema to Drizzle
393
- this.connector = drizzle({
394
- client: new Pool(this.settings.connection),
395
- schema: this.schema, // This now contains both tables AND relations
396
- });
397
- }
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
+ );
398
214
 
399
- override async connect(): Promise<TNodePostgresConnector | undefined> {
400
- await (this.connector.client as Pool).connect();
401
- return this.connector;
215
+ const client = new Pool(this.settings);
216
+ this.connector = drizzle({ client, schema });
402
217
  }
403
218
 
404
- override async disconnect(): Promise<void> {
405
- 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}`;
406
222
  }
407
223
  }
408
224
  ```
409
225
 
410
- #### Why This Pattern?
226
+ **How auto-discovery works:**
411
227
 
412
- **Without the relations merged in:**
413
- ```typescript
414
- // WRONG - Relations won't work!
415
- schema: {
416
- [User.TABLE_NAME]: userTable,
417
- [Configuration.TABLE_NAME]: configurationTable,
418
- }
419
- ```
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
420
231
 
421
- **Result:** Your repository's `include` queries will fail. You won't be able to fetch related data.
232
+ ### Manual Schema (Optional)
422
233
 
423
- **With tables and relations merged:**
424
- ```typescript
425
- // ✅ CORRECT - Relations work perfectly!
426
- schema: Object.assign(
427
- {},
428
- {
429
- [User.TABLE_NAME]: userTable,
430
- [Configuration.TABLE_NAME]: configurationTable,
431
- },
432
- userRelations.relations,
433
- configurationRelations.relations,
434
- )
435
- ```
234
+ If you need explicit control, you can still provide schema manually:
436
235
 
437
- **Result:** You can now do:
438
236
  ```typescript
439
- const config = await configRepo.findOne({
440
- filter: {
441
- where: { id: '123' },
442
- include: [{ relation: 'creator' }], // This works!
443
- },
444
- });
445
- console.log(config.creator.name); // Access related User data
446
- ```
447
-
448
- #### Adding New Models to Your DataSource
449
-
450
- Every time you create a new model, you need to:
451
-
452
- 1. Import its table and relations
453
- 2. Add them to the schema object
454
-
455
- **Example - Adding a `Post` model:**
456
- ```typescript
457
- import {
458
- Post,
459
- postTable,
460
- postRelations,
461
- // ... other models
462
- } from '@/models/entities';
463
-
464
- // In your constructor:
465
- schema: Object.assign(
466
- {},
467
- {
468
- [User.TABLE_NAME]: userTable,
469
- [Configuration.TABLE_NAME]: configurationTable,
470
- [Post.TABLE_NAME]: postTable, // Add the table
471
- },
472
- userRelations.relations,
473
- configurationRelations.relations,
474
- postRelations.relations, // Add the relations
475
- ),
476
- ```
477
-
478
- **Pro tip:** As your app grows, consider using a helper function to reduce boilerplate:
479
-
480
- ```typescript
481
- function buildSchema(models: Array<{ table: any; relations: any; name: string }>) {
482
- const tables = models.reduce((acc, m) => ({ ...acc, [m.name]: m.table }), {});
483
- const relations = models.map(m => m.relations.relations);
484
- return Object.assign({}, tables, ...relations);
237
+ @datasource({ driver: 'node-postgres' })
238
+ export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
239
+ constructor() {
240
+ super({
241
+ name: PostgresDataSource.name,
242
+ config: { /* ... */ },
243
+ schema: {
244
+ User: User.schema,
245
+ Configuration: Configuration.schema,
246
+ // Add relations if using Drizzle's relational queries
247
+ },
248
+ });
249
+ }
485
250
  }
486
-
487
- // Usage:
488
- schema: buildSchema([
489
- { name: User.TABLE_NAME, table: userTable, relations: userRelations },
490
- { name: Configuration.TABLE_NAME, table: configurationTable, relations: configurationRelations },
491
- { name: Post.TABLE_NAME, table: postTable, relations: postRelations },
492
- ])
493
251
  ```
494
252
 
495
253
  ### Registering a DataSource
496
254
 
497
- Finally, register your `DataSource` with the application in `src/application.ts`.
498
-
499
255
  ```typescript
500
256
  // src/application.ts
501
- import { PostgresDataSource } from './datasources';
502
-
503
257
  export class Application extends BaseApplication {
504
- // ...
505
258
  preConfigure(): ValueOrPromise<void> {
506
259
  this.dataSource(PostgresDataSource);
507
- // ...
508
260
  }
509
261
  }
510
262
  ```
511
263
 
512
- > **Deep Dive:**
513
- > - Explore the [**`BaseDataSource`**](../../references/base/datasources.md) class.
514
-
515
264
  ---
516
265
 
517
266
  ## 3. Repositories: The Data Access Layer
518
267
 
519
- Repositories abstract the data access logic. They use the configured `DataSource` to perform type-safe queries against the database.
268
+ Repositories provide type-safe CRUD operations. Use `@repository` decorator with both `model` and `dataSource` for auto-discovery.
520
269
 
521
- ### Creating a Repository
270
+ ### Pattern 1: Zero Boilerplate (Recommended)
522
271
 
523
- A repository extends `DefaultCRUDRepository` (for full read/write operations), is decorated with `@repository`, and injects the `DataSource`.
272
+ The simplest approach - everything is auto-resolved:
524
273
 
525
274
  ```typescript
526
275
  // src/repositories/configuration.repository.ts
527
- import {
528
- Configuration,
529
- configurationRelations, // Import configurationRelations
530
- TConfigurationSchema,
531
- } from '@/models/entities';
532
- import { IDataSource, inject, repository, DefaultCRUDRepository } from '@venizia/ignis';
533
-
534
- // Decorator to mark this class as a repository for DI
535
- @repository({})
536
- export class ConfigurationRepository extends DefaultCRUDRepository<TConfigurationSchema> {
276
+ import { Configuration } from '@/models/entities';
277
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
278
+ import { DefaultCRUDRepository, repository } from '@venizia/ignis';
279
+
280
+ @repository({
281
+ model: Configuration,
282
+ dataSource: PostgresDataSource,
283
+ })
284
+ export class ConfigurationRepository extends DefaultCRUDRepository<typeof Configuration.schema> {
285
+ // No constructor needed!
286
+
287
+ async findByCode(code: string) {
288
+ return this.findOne({ filter: { where: { code } } });
289
+ }
290
+
291
+ async findByGroup(group: string) {
292
+ return this.find({ filter: { where: { group } } });
293
+ }
294
+ }
295
+ ```
296
+
297
+ ### Pattern 2: Explicit @inject
298
+
299
+ When you need constructor control (e.g., read-only repository or additional dependencies):
300
+
301
+ ```typescript
302
+ // src/repositories/user.repository.ts
303
+ import { User } from '@/models/entities';
304
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
305
+ import { inject, ReadableRepository, repository } from '@venizia/ignis';
306
+ import { CacheService } from '@/services/cache.service';
307
+
308
+ @repository({ model: User, dataSource: PostgresDataSource })
309
+ export class UserRepository extends ReadableRepository<typeof User.schema> {
537
310
  constructor(
538
- // Inject the configured datasource
539
- @inject({ key: 'datasources.PostgresDataSource' }) dataSource: IDataSource,
311
+ // First parameter MUST be DataSource injection
312
+ @inject({ key: 'datasources.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,
540
318
  ) {
541
- // Pass the datasource, the model's Entity class, AND the relations definitions to the super constructor
542
- super({ dataSource, entityClass: Configuration, relations: configurationRelations.definitions });
319
+ super(dataSource);
320
+ }
321
+
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
+
329
+ return this.findOne({ filter: { where: { realm } } });
543
330
  }
544
331
  }
545
332
  ```
546
- You would then register this repository in your `application.ts`: `this.repository(ConfigurationRepository);`
547
333
 
548
- ### Querying Data
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
338
+
339
+ ### Repository Types
549
340
 
550
- Repositories provide a full suite of type-safe methods for CRUD operations using a standardized `filter` object.
341
+ | Type | Description |
342
+ |------|-------------|
343
+ | `DefaultCRUDRepository` | Full read/write operations |
344
+ | `ReadableRepository` | Read-only operations |
345
+ | `PersistableRepository` | Write operations only |
346
+
347
+ ### Querying Data
551
348
 
552
349
  ```typescript
553
- // Example usage in application.ts or a service
554
350
  const repo = this.get<ConfigurationRepository>({ key: 'repositories.ConfigurationRepository' });
555
351
 
556
352
  // Find multiple records
557
- const someConfigs = await repo.find({
353
+ const configs = await repo.find({
558
354
  filter: {
559
355
  where: { group: 'SYSTEM' },
560
356
  limit: 10,
@@ -562,30 +358,186 @@ const someConfigs = await repo.find({
562
358
  }
563
359
  });
564
360
 
565
- // 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
566
367
  const newConfig = await repo.create({
567
368
  data: {
568
- code: 'NEW_CODE',
369
+ code: 'NEW_SETTING',
569
370
  group: 'SYSTEM',
570
- // ... other fields
371
+ description: 'A new setting',
571
372
  }
572
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' });
573
383
  ```
574
384
 
575
385
  ### Querying with Relations
576
386
 
577
- 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`:
578
388
 
579
389
  ```typescript
580
- const resultsWithCreator = await repo.find({
390
+ const configWithCreator = await repo.findOne({
581
391
  filter: {
582
- where: { code: 'some_code' },
583
- include: [{ relation: 'creator' }], // Fetch the related user
392
+ where: { code: 'APP_NAME' },
393
+ include: [{ relation: 'creator' }],
584
394
  },
585
395
  });
586
396
 
587
- if (resultsWithCreator.length > 0) {
588
- // `resultsWithCreator[0].creator` will contain the full User object
589
- 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
+ }
410
+ }
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[] => [];
590
525
  }
591
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)