@venizia/ignis-docs 0.0.1 → 0.0.3

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.
@@ -8,15 +8,17 @@ Technical reference for DataSource classes - managing database connections in Ig
8
8
 
9
9
  | Class/Interface | Purpose | Key Members |
10
10
  |-----------------|---------|-------------|
11
- | **IDataSource** | Contract for all datasources | `name`, `settings`, `connector`, `getSchema()`, `configure()` |
11
+ | **IDataSource** | Contract for all datasources | `name`, `settings`, `connector`, `getSchema()`, `configure()`, `beginTransaction()` |
12
12
  | **AbstractDataSource** | Base implementation with logging | Extends `BaseHelper` |
13
- | **BaseDataSource** | Concrete class to extend | Auto-discovery, driver from decorator |
13
+ | **BaseDataSource** | Concrete class to extend | Auto-discovery, driver from decorator, transaction support |
14
+ | **ITransaction** | Transaction object | `connector`, `isActive`, `commit()`, `rollback()` |
15
+ | **IsolationLevels** | Isolation level constants | `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE` |
14
16
 
15
17
  ## `IDataSource` Interface
16
18
 
17
19
  Contract for all datasource classes in the framework.
18
20
 
19
- **File:** `packages/core/src/base/datasources/types.ts`
21
+ **File:** `packages/core/src/base/datasources/common/types.ts`
20
22
 
21
23
  ### Properties & Methods
22
24
 
@@ -24,10 +26,14 @@ Contract for all datasource classes in the framework.
24
26
  |--------|------|-------------|
25
27
  | `name` | `string` | Datasource name |
26
28
  | `settings` | `object` | Configuration object |
27
- | `connector` | `TDatabaseConnector` | Database connector instance (e.g., Drizzle) |
28
- | `getSchema()` | Method | Returns combined Drizzle schema (auto-discovered or manual) |
29
+ | `connector` | `TNodePostgresConnector` | Database connector instance (Drizzle) |
30
+ | `schema` | `Schema` | Combined Drizzle schema (auto-discovered or manual) |
31
+ | `getSchema()` | Method | Returns combined Drizzle schema |
32
+ | `getSettings()` | Method | Returns connection settings |
33
+ | `getConnector()` | Method | Returns the Drizzle connector |
29
34
  | `configure()` | Method | Initializes the `connector` |
30
35
  | `getConnectionString()` | Method | Returns connection string |
36
+ | `beginTransaction(opts?)` | Method | Starts a new database transaction |
31
37
 
32
38
  ## `AbstractDataSource` & `BaseDataSource`
33
39
 
@@ -264,4 +270,71 @@ export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, I
264
270
  | `getConnector()` | Returns the Drizzle connector |
265
271
  | `hasDiscoverableModels()` | Returns `true` if there are models registered for this datasource |
266
272
 
273
+ ## Transaction Support
274
+
275
+ DataSources provide built-in transaction management through the `beginTransaction()` method. This allows you to perform atomic operations across multiple repositories.
276
+
277
+ ### Transaction Types
278
+
279
+ **File:** `packages/core/src/base/datasources/common/types.ts`
280
+
281
+ | Type | Description |
282
+ |------|-------------|
283
+ | `ITransaction<Schema>` | Transaction object with `commit()`, `rollback()`, and `connector` |
284
+ | `ITransactionOptions` | Options for starting a transaction (e.g., `isolationLevel`) |
285
+ | `TIsolationLevel` | Union type: `'READ COMMITTED'` \| `'REPEATABLE READ'` \| `'SERIALIZABLE'` |
286
+ | `IsolationLevels` | Static class with isolation level constants and validation |
287
+
288
+ ### ITransaction Interface
289
+
290
+ ```typescript
291
+ interface ITransaction<Schema> {
292
+ connector: TNodePostgresTransactionConnector<Schema>;
293
+ isActive: boolean;
294
+ isolationLevel: TIsolationLevel;
295
+
296
+ commit(): Promise<void>;
297
+ rollback(): Promise<void>;
298
+ }
299
+ ```
300
+
301
+ ### Isolation Levels
302
+
303
+ Use the `IsolationLevels` static class for type-safe isolation level constants:
304
+
305
+ ```typescript
306
+ import { IsolationLevels } from '@venizia/ignis';
307
+
308
+ // Available levels
309
+ IsolationLevels.READ_COMMITTED // Default - prevents dirty reads
310
+ IsolationLevels.REPEATABLE_READ // Consistent reads within transaction
311
+ IsolationLevels.SERIALIZABLE // Strictest isolation
312
+
313
+ // Validation
314
+ IsolationLevels.isValid('READ COMMITTED'); // true
315
+ IsolationLevels.isValid('INVALID'); // false
316
+ ```
317
+
318
+ ### Usage Example
319
+
320
+ ```typescript
321
+ // Start transaction from datasource or repository
322
+ const tx = await dataSource.beginTransaction({
323
+ isolationLevel: IsolationLevels.SERIALIZABLE
324
+ });
325
+
326
+ try {
327
+ // Use tx.connector for operations
328
+ await tx.connector.insert(userTable).values({ name: 'Alice' });
329
+ await tx.connector.insert(profileTable).values({ userId: '...', bio: 'Hello' });
330
+
331
+ await tx.commit();
332
+ } catch (error) {
333
+ await tx.rollback();
334
+ throw error;
335
+ }
336
+ ```
337
+
338
+ > **Note:** For most use cases, prefer using `repository.beginTransaction()` which provides a higher-level API. See [Repositories Reference](./repositories.md#transactions) for details.
339
+
267
340
  This architecture ensures that datasources are configured consistently and that the fully-initialized Drizzle connector, aware of all schemas and relations, is available to repositories for querying.
@@ -321,9 +321,86 @@ const results = await repo.createAll({
321
321
  // Type: Promise<TCount & { data: User[] }>
322
322
  ```
323
323
 
324
+ ### Generic Return Types
325
+
326
+ For queries involving relations (`include`) or custom mapped types, you can override the return type of repository methods. This provides stronger type safety at the application layer without losing the convenience of the repository pattern.
327
+
328
+ ```typescript
329
+ // Define custom return type with relations
330
+ type ProductWithChannels = Product & {
331
+ saleChannelProducts: (SaleChannelProduct & {
332
+ saleChannel: SaleChannel
333
+ })[]
334
+ };
335
+
336
+ // Use generic override
337
+ const product = await productRepo.findOne<ProductWithChannels>({
338
+ filter: {
339
+ where: { id: '...' },
340
+ include: [{
341
+ relation: 'saleChannelProducts',
342
+ scope: { include: [{ relation: 'saleChannel' }] }
343
+ }]
344
+ }
345
+ });
346
+
347
+ // TypeScript knows the structure!
348
+ if (product) {
349
+ console.log(product.saleChannelProducts[0].saleChannel.name);
350
+ }
351
+ ```
352
+
353
+ **Supported Methods:**
354
+ - `find<R>()`
355
+ - `findOne<R>()`
356
+ - `findById<R>()`
357
+ - `create<R>()`, `createAll<R>()`
358
+ - `updateById<R>()`, `updateAll<R>()`
359
+ - `deleteById<R>()`, `deleteAll<R>()`
360
+
361
+ ### Transactions
362
+
363
+ Repositories provide direct access to transaction management via the `beginTransaction()` method. This allows you to orchestrate atomic operations across multiple repositories or services.
364
+
365
+ ```typescript
366
+ // Start a transaction
367
+ const tx = await repo.beginTransaction();
368
+
369
+ try {
370
+ // Perform operations within the transaction
371
+ const user = await userRepo.create({
372
+ data: { name: 'Alice' },
373
+ options: { transaction: tx } // Pass the transaction object
374
+ });
375
+
376
+ const profile = await profileRepo.create({
377
+ data: { userId: user.id, bio: 'Hello' },
378
+ options: { transaction: tx } // Use the same transaction
379
+ });
380
+
381
+ // Commit changes
382
+ await tx.commit();
383
+ } catch (error) {
384
+ // Rollback on error
385
+ await tx.rollback();
386
+ throw error;
387
+ }
388
+ ```
389
+
390
+ **Isolation Levels:**
391
+ You can specify the isolation level when starting a transaction:
392
+ ```typescript
393
+ const tx = await repo.beginTransaction({
394
+ isolationLevel: 'SERIALIZABLE' // 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE'
395
+ });
396
+ ```
397
+
324
398
  ### Relations Auto-Resolution
325
399
 
326
- Relations are now automatically resolved from the entity's static `relations` property:
400
+ Relations are automatically resolved from the entity's static `relations` property. This resolution is **recursive**, allowing for deeply nested `include` queries across multiple levels of the entity graph.
401
+
402
+ > [!WARNING]
403
+ > **Performance Recommendation:** Each nested `include` adds significant overhead to SQL generation and result mapping. We strongly recommend a **maximum of 2 levels** (e.g., `Product -> Junction -> SaleChannel`). For deeper relationships, fetching data in multiple smaller queries is often more performant.
327
404
 
328
405
  ```typescript
329
406
  // Define entity with static relations
@@ -339,11 +416,17 @@ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
339
416
  // No need to pass relations in constructor - auto-resolved!
340
417
  }
341
418
 
342
- // Relations are available for include queries
343
- const users = await repo.find({
419
+ // Nested inclusion (Product -> Junction -> SaleChannel)
420
+ const product = await repo.findOne({
344
421
  filter: {
345
- where: { status: 'active' },
346
- include: [{ relation: 'posts' }], // Works automatically
422
+ include: [
423
+ {
424
+ relation: 'saleChannelProducts', // Level 1
425
+ scope: {
426
+ include: [{ relation: 'saleChannel' }] // Level 2 (Nested)
427
+ }
428
+ }
429
+ ]
347
430
  }
348
431
  });
349
432
  ```
@@ -363,6 +446,9 @@ The `getQueryInterface()` method validates that the entity's schema is properly
363
446
 
364
447
  The `ReadableRepository` automatically optimizes flat queries (no relations, no field selection) using Drizzle's Core API instead of Query API. This provides ~15-20% performance improvement for simple queries.
365
448
 
449
+ > [!IMPORTANT]
450
+ > **Always use `limit`:** To ensure consistent performance and prevent memory exhaustion, always provide a `limit` in your filter options, especially for public-facing endpoints.
451
+
366
452
  **Automatic Optimization:**
367
453
 
368
454
  ```typescript
@@ -370,7 +456,7 @@ The `ReadableRepository` automatically optimizes flat queries (no relations, no
370
456
  const users = await repo.find({
371
457
  filter: {
372
458
  where: { status: 'active' },
373
- limit: 10,
459
+ limit: 10, // ✅ Mandatory limit for performance
374
460
  order: ['createdAt DESC'],
375
461
  }
376
462
  });
@@ -6,6 +6,7 @@ Reusable classes and functions providing common functionality - designed for eas
6
6
 
7
7
  | Helper | Purpose | Key Features |
8
8
  |--------|---------|--------------|
9
+ | [Common Types](./types.md) | Utility types | Nullable, resolvers, class types |
9
10
  | [Cron](./cron.md) | Job scheduling | Cron expressions, task management |
10
11
  | [Crypto](./crypto.md) | Cryptographic operations | Hashing, AES/RSA encryption/decryption |
11
12
  | [Environment](./env.md) | Environment variables | Centralized config access |
@@ -0,0 +1,151 @@
1
+ # Common Types
2
+
3
+ Utility types and helper functions for common TypeScript patterns.
4
+
5
+ ## Value Resolution Types
6
+
7
+ Types for lazy/deferred value resolution patterns.
8
+
9
+ ### TResolver / TAsyncResolver
10
+
11
+ ```typescript
12
+ type TResolver<T> = (...args: any[]) => T;
13
+ type TAsyncResolver<T> = (...args: any[]) => T | Promise<T>;
14
+ ```
15
+
16
+ Function types that resolve to a value. `TAsyncResolver` supports async functions.
17
+
18
+ ### TValueOrResolver / TValueOrAsyncResolver
19
+
20
+ ```typescript
21
+ type TValueOrResolver<T> = T | TResolver<T>;
22
+ type TValueOrAsyncResolver<T> = T | TAsyncResolver<T>;
23
+ ```
24
+
25
+ Union types allowing either a direct value or a resolver function.
26
+
27
+ **Usage:**
28
+
29
+ ```typescript
30
+ import { TValueOrAsyncResolver, resolveValueAsync } from '@venizia/ignis-helpers';
31
+
32
+ interface DatabaseConfig {
33
+ host: string;
34
+ port: number;
35
+ }
36
+
37
+ type ConfigOption = TValueOrAsyncResolver<DatabaseConfig>;
38
+
39
+ // Direct value
40
+ const config1: ConfigOption = { host: 'localhost', port: 5432 };
41
+
42
+ // Sync resolver
43
+ const config2: ConfigOption = () => ({ host: 'localhost', port: 5432 });
44
+
45
+ // Async resolver
46
+ const config3: ConfigOption = async () => {
47
+ const config = await fetchConfigFromVault();
48
+ return config;
49
+ };
50
+
51
+ // Resolve any of the above
52
+ const resolved = await resolveValueAsync(config3);
53
+ ```
54
+
55
+ ### resolveValue / resolveValueAsync
56
+
57
+ ```typescript
58
+ const resolveValue: <T>(valueOrResolver: TValueOrResolver<T>) => T;
59
+ const resolveValueAsync: <T>(valueOrResolver: TValueOrAsyncResolver<T>) => Promise<T>;
60
+ ```
61
+
62
+ Helper functions to resolve lazy values. They handle:
63
+ - **Direct values**: returned as-is
64
+ - **Class constructors**: returned as-is (not invoked)
65
+ - **Resolver functions**: invoked and result returned
66
+
67
+ ## Nullable Types
68
+
69
+ ### TNullable
70
+
71
+ ```typescript
72
+ type TNullable<T> = T | undefined | null;
73
+ ```
74
+
75
+ Makes a type nullable (can be `undefined` or `null`).
76
+
77
+ ### ValueOrPromise
78
+
79
+ ```typescript
80
+ type ValueOrPromise<T> = T | Promise<T>;
81
+ ```
82
+
83
+ A value that may or may not be wrapped in a Promise.
84
+
85
+ ## Class Types
86
+
87
+ ### TConstructor / TAbstractConstructor
88
+
89
+ ```typescript
90
+ type TConstructor<T> = new (...args: any[]) => T;
91
+ type TAbstractConstructor<T> = abstract new (...args: any[]) => T;
92
+ ```
93
+
94
+ Types representing class constructors.
95
+
96
+ ### TClass / TAbstractClass
97
+
98
+ ```typescript
99
+ type TClass<T> = TConstructor<T> & { [property: string]: any };
100
+ type TAbstractClass<T> = TAbstractConstructor<T> & { [property: string]: any };
101
+ ```
102
+
103
+ Class types with static properties.
104
+
105
+ ### TMixinTarget / TAbstractMixinTarget
106
+
107
+ ```typescript
108
+ type TMixinTarget<T> = TConstructor<{ [P in keyof T]: T[P] }>;
109
+ type TAbstractMixinTarget<T> = TAbstractConstructor<{ [P in keyof T]: T[P] }>;
110
+ ```
111
+
112
+ Types for mixin pattern targets.
113
+
114
+ ## Object Utility Types
115
+
116
+ ### ValueOf
117
+
118
+ ```typescript
119
+ type ValueOf<T> = T[keyof T];
120
+ ```
121
+
122
+ Extracts the value types from an object type.
123
+
124
+ ### ValueOptional / ValueOptionalExcept
125
+
126
+ ```typescript
127
+ type ValueOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
128
+ type ValueOptionalExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;
129
+ ```
130
+
131
+ Make specific keys optional while keeping others required, or vice versa.
132
+
133
+ ### TPrettify
134
+
135
+ ```typescript
136
+ type TPrettify<T> = { [K in keyof T]: T[K] } & {};
137
+ ```
138
+
139
+ Flattens intersection types for better IDE display.
140
+
141
+ ## Const Value Types
142
+
143
+ ### TStringConstValue / TNumberConstValue / TConstValue
144
+
145
+ ```typescript
146
+ type TStringConstValue<T extends TClass<any>> = Extract<ValueOf<T>, string>;
147
+ type TNumberConstValue<T extends TClass<any>> = Extract<ValueOf<T>, number>;
148
+ type TConstValue<T extends TClass<any>> = Extract<ValueOf<T>, string | number>;
149
+ ```
150
+
151
+ Extract constant value types from a class.
@@ -1,216 +0,0 @@
1
- ---
2
- title: Planned - Transaction Support
3
- description: Implementation plan for Loopback 4-style explicit transaction objects
4
- ---
5
-
6
- # Planned: Transaction Support
7
-
8
- **Status:** Planned (Not Yet Implemented)
9
- **Priority:** Future Enhancement
10
-
11
- ## Goal
12
-
13
- Implement Loopback 4-style explicit transaction objects, allowing transactions to be passed through multiple services/repositories instead of using Drizzle's callback-based approach.
14
-
15
- ## Target API
16
-
17
- ```typescript
18
- // Default isolation level (READ COMMITTED)
19
- const tx = await userRepo.beginTransaction();
20
-
21
- // Or with specific isolation level
22
- const tx = await userRepo.beginTransaction({
23
- isolationLevel: 'SERIALIZABLE'
24
- });
25
-
26
- try {
27
- await userRepo.create({ data, options: { transaction: tx } });
28
- await profileRepo.create({ data, options: { transaction: tx } });
29
- await tx.commit();
30
- } catch (err) {
31
- await tx.rollback();
32
- throw err;
33
- }
34
- ```
35
-
36
- ### Isolation Levels
37
-
38
- | Level | Description | Use Case |
39
- |-------|-------------|----------|
40
- | `READ COMMITTED` | Default. Sees only committed data at query start | General use, most common |
41
- | `REPEATABLE READ` | Sees snapshot from transaction start | Reports, consistent reads |
42
- | `SERIALIZABLE` | Strictest. Full isolation, may throw serialization errors | Financial transactions, critical data |
43
-
44
- ---
45
-
46
- ## Implementation Steps
47
-
48
- ### Step 1: Define Transaction Types
49
-
50
- **File:** `packages/core/src/base/datasources/types.ts`
51
-
52
- ```typescript
53
- /** PostgreSQL transaction isolation levels */
54
- export type TIsolationLevel = 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE';
55
-
56
- /** Options for starting a transaction */
57
- export interface ITransactionOptions {
58
- isolationLevel?: TIsolationLevel;
59
- }
60
-
61
- /** Transaction object returned by beginTransaction() */
62
- export interface ITransaction<Connector = TNodePostgresConnector> {
63
- /** Isolated Drizzle instance bound to this transaction */
64
- connector: Connector;
65
-
66
- /** Commit the transaction */
67
- commit(): Promise<void>;
68
-
69
- /** Rollback the transaction */
70
- rollback(): Promise<void>;
71
-
72
- /** Check if transaction is still active */
73
- isActive: boolean;
74
-
75
- /** The isolation level used for this transaction */
76
- isolationLevel: TIsolationLevel;
77
- }
78
- ```
79
-
80
- ### Step 2: Add `beginTransaction()` to DataSource
81
-
82
- **File:** `packages/core/src/base/datasources/base.ts`
83
-
84
- ```typescript
85
- async beginTransaction(
86
- opts?: ITransactionOptions
87
- ): Promise<ITransaction<Connector>> {
88
- // 1. Get raw client from pool
89
- const pool = this.connector.client as Pool;
90
- const client = await pool.connect();
91
-
92
- // 2. Determine isolation level (default: READ COMMITTED)
93
- const isolationLevel: TIsolationLevel = opts?.isolationLevel ?? 'READ COMMITTED';
94
-
95
- // 3. Execute BEGIN with isolation level
96
- await client.query(`BEGIN TRANSACTION ISOLATION LEVEL ${isolationLevel}`);
97
-
98
- // 4. Create isolated Drizzle instance with this client
99
- const txConnector = drizzle({ client, schema: this.schema });
100
-
101
- // 5. Return transaction object
102
- let isActive = true;
103
-
104
- return {
105
- connector: txConnector as Connector,
106
- isActive,
107
- isolationLevel,
108
-
109
- async commit() {
110
- if (!isActive) throw new Error('Transaction already ended');
111
- try {
112
- await client.query('COMMIT');
113
- } finally {
114
- isActive = false;
115
- client.release();
116
- }
117
- },
118
-
119
- async rollback() {
120
- if (!isActive) throw new Error('Transaction already ended');
121
- try {
122
- await client.query('ROLLBACK');
123
- } finally {
124
- isActive = false;
125
- client.release();
126
- }
127
- },
128
- };
129
- }
130
- ```
131
-
132
- ### Step 3: Update Repository Base
133
-
134
- **File:** `packages/core/src/base/repositories/core/base.ts`
135
-
136
- ```typescript
137
- // Add method to start transaction (delegates to DataSource)
138
- async beginTransaction(opts?: ITransactionOptions): Promise<ITransaction> {
139
- return this.dataSource.beginTransaction(opts);
140
- }
141
-
142
- // Replace this.connector with getConnector(opts)
143
- protected getConnector(opts?: { transaction?: ITransaction }) {
144
- if (opts?.transaction) {
145
- if (!opts.transaction.isActive) {
146
- throw getError({ message: 'Transaction is no longer active' });
147
- }
148
- return opts.transaction.connector;
149
- }
150
- return this.dataSource.connector;
151
- }
152
- ```
153
-
154
- ### Step 4: Update CRUD Options Types
155
-
156
- **File:** `packages/core/src/base/repositories/common/types.ts`
157
-
158
- ```typescript
159
- export type TTransactionOption = {
160
- transaction?: ITransaction;
161
- };
162
-
163
- // Add to existing option types
164
- export type TCreateOptions = TTransactionOption & {
165
- shouldReturn?: boolean;
166
- log?: TRepositoryLogOptions;
167
- };
168
- ```
169
-
170
- ### Step 5: Update CRUD Methods
171
-
172
- **Files:** `readable.ts`, `persistable.ts`
173
-
174
- Change all methods from:
175
- ```typescript
176
- this.connector.insert(...)
177
- ```
178
-
179
- To:
180
- ```typescript
181
- this.getConnector(opts.options).insert(...)
182
- ```
183
-
184
- ---
185
-
186
- ## Files to Modify
187
-
188
- | File | Changes |
189
- |------|---------|
190
- | `packages/core/src/base/datasources/types.ts` | Add `TIsolationLevel`, `ITransactionOptions`, `ITransaction` |
191
- | `packages/core/src/base/datasources/base.ts` | Add `beginTransaction(opts?)` method |
192
- | `packages/core/src/base/repositories/common/types.ts` | Add `TTransactionOption` |
193
- | `packages/core/src/base/repositories/core/base.ts` | Add `beginTransaction(opts?)`, `getConnector(opts)` |
194
- | `packages/core/src/base/repositories/core/readable.ts` | Use `getConnector(opts)` in all methods |
195
- | `packages/core/src/base/repositories/core/persistable.ts` | Use `getConnector(opts)` in all methods |
196
-
197
- ---
198
-
199
- ## Breaking Changes
200
-
201
- 1. **`this.connector`** → `this.getConnector(opts)`
202
- - Backward compatible when called without args
203
-
204
- 2. **Options parameter** - Now includes optional `transaction` field
205
- - Non-breaking: transaction is optional
206
-
207
- ---
208
-
209
- ## Benefits
210
-
211
- | Aspect | Current (Drizzle Callback) | After (Pass-through) |
212
- |--------|---------------------------|----------------------|
213
- | Service composition | Hard - all in one callback | Easy - pass tx anywhere |
214
- | Separation of concerns | Services must know each other | Services stay independent |
215
- | Testing | Complex mocking | Easy to mock tx object |
216
- | Code organization | Nested callbacks | Flat, sequential flow |