@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.
- package/package.json +2 -2
- package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +86 -0
- package/wiki/changelogs/2025-12-26-transaction-support.md +57 -0
- package/wiki/changelogs/index.md +3 -1
- package/wiki/changelogs/planned-schema-migrator.md +561 -0
- package/wiki/get-started/best-practices/code-style-standards.md +651 -10
- package/wiki/get-started/best-practices/performance-optimization.md +11 -2
- package/wiki/get-started/core-concepts/components.md +59 -42
- package/wiki/get-started/core-concepts/persistent.md +43 -47
- package/wiki/references/base/components.md +515 -31
- package/wiki/references/base/controllers.md +85 -18
- package/wiki/references/base/datasources.md +78 -5
- package/wiki/references/base/repositories.md +92 -6
- package/wiki/references/helpers/index.md +1 -0
- package/wiki/references/helpers/types.md +151 -0
- package/wiki/changelogs/planned-transaction-support.md +0 -216
|
@@ -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` | `
|
|
28
|
-
| `
|
|
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
|
|
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
|
-
//
|
|
343
|
-
const
|
|
419
|
+
// Nested inclusion (Product -> Junction -> SaleChannel)
|
|
420
|
+
const product = await repo.findOne({
|
|
344
421
|
filter: {
|
|
345
|
-
|
|
346
|
-
|
|
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 |
|