@onivoro/server-typeorm-postgres 24.0.0 → 24.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @onivoro/server-typeorm-postgres
2
2
 
3
- A comprehensive TypeORM PostgreSQL integration library for NestJS applications, providing custom repositories, migration utilities, decorators, and enhanced PostgreSQL-specific functionality for enterprise-scale database operations.
3
+ A TypeORM PostgreSQL integration library providing repository patterns, SQL generation utilities, migration base classes, and PostgreSQL/Redshift-specific optimizations for enterprise-scale applications.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,43 +10,30 @@ npm install @onivoro/server-typeorm-postgres
10
10
 
11
11
  ## Features
12
12
 
13
- - **TypeORM PostgreSQL Module**: Complete NestJS module for PostgreSQL integration
14
- - **Custom Repository Classes**: Enhanced repository patterns with pagination and utilities
15
- - **Migration Base Classes**: Structured migration classes for database schema management
16
- - **Custom Decorators**: PostgreSQL-specific column decorators and table definitions
17
- - **Redshift Support**: Amazon Redshift repository integration
18
- - **SQL Writer Utilities**: Advanced SQL generation and execution utilities
19
- - **Data Source Factory**: Flexible data source configuration and creation
20
- - **Pagination Support**: Built-in pagination utilities and interfaces
13
+ - **TypeORM Repository Pattern**: Enhanced repository with PostgreSQL-specific features
14
+ - **Redshift Repository**: Specialized repository for Amazon Redshift operations
15
+ - **SQL Writer**: Static utility class for PostgreSQL DDL generation
16
+ - **Migration Base Classes**: Simplified migration classes for common operations
17
+ - **Custom Decorators**: Table and column decorators for entity definitions
18
+ - **Pagination Support**: Abstract paging repository for custom implementations
21
19
  - **Type Safety**: Full TypeScript support with comprehensive type definitions
22
- - **PostgreSQL Optimizations**: PostgreSQL-specific optimizations and best practices
23
20
 
24
21
  ## Quick Start
25
22
 
26
- ### Import the Module
23
+ ### Module Import
27
24
 
28
25
  ```typescript
29
26
  import { ServerTypeormPostgresModule } from '@onivoro/server-typeorm-postgres';
30
27
 
31
28
  @Module({
32
29
  imports: [
33
- ServerTypeormPostgresModule.forRoot({
34
- host: 'localhost',
35
- port: 5432,
36
- username: 'postgres',
37
- password: 'password',
38
- database: 'myapp',
39
- entities: [User, Product, Order],
40
- synchronize: false,
41
- logging: true,
42
- ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
43
- })
30
+ ServerTypeormPostgresModule
44
31
  ],
45
32
  })
46
33
  export class AppModule {}
47
34
  ```
48
35
 
49
- ### Define Entities with Custom Decorators
36
+ ### Entity Definition with Custom Decorators
50
37
 
51
38
  ```typescript
52
39
  import {
@@ -55,10 +42,8 @@ import {
55
42
  TableColumn,
56
43
  NullableTableColumn
57
44
  } from '@onivoro/server-typeorm-postgres';
58
- import { Entity } from 'typeorm';
59
45
 
60
- @Entity()
61
- @Table('users')
46
+ @Table({ name: 'users' })
62
47
  export class User {
63
48
  @PrimaryTableColumn()
64
49
  id: number;
@@ -69,850 +54,671 @@ export class User {
69
54
  @TableColumn({ type: 'varchar', length: 100 })
70
55
  firstName: string;
71
56
 
72
- @TableColumn({ type: 'varchar', length: 100 })
73
- lastName: string;
74
-
75
57
  @NullableTableColumn({ type: 'timestamp' })
76
58
  lastLoginAt?: Date;
77
59
 
78
60
  @TableColumn({ type: 'boolean', default: true })
79
61
  isActive: boolean;
80
62
 
81
- @TableColumn({ type: 'jsonb' })
63
+ @TableColumn({ type: 'jsonb', default: '{}' })
82
64
  metadata: Record<string, any>;
83
65
 
84
66
  @TableColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
85
67
  createdAt: Date;
86
68
 
87
- @TableColumn({
88
- type: 'timestamp',
89
- default: () => 'CURRENT_TIMESTAMP',
90
- onUpdate: 'CURRENT_TIMESTAMP'
91
- })
92
- updatedAt: Date;
93
-
94
69
  @NullableTableColumn({ type: 'timestamp' })
95
70
  deletedAt?: Date;
96
71
  }
97
72
  ```
98
73
 
99
- ### Use Custom Repository
74
+ ## Repository Classes
75
+
76
+ ### TypeOrmRepository
77
+
78
+ Enhanced repository with PostgreSQL-specific methods:
100
79
 
101
80
  ```typescript
102
81
  import { Injectable } from '@nestjs/common';
103
- import { TypeOrmRepository, TypeOrmPagingRepository } from '@onivoro/server-typeorm-postgres';
82
+ import { TypeOrmRepository } from '@onivoro/server-typeorm-postgres';
83
+ import { EntityManager } from 'typeorm';
104
84
  import { User } from './user.entity';
105
85
 
106
86
  @Injectable()
107
- export class UserRepository extends TypeOrmPagingRepository<User> {
108
- constructor() {
109
- super(User);
87
+ export class UserRepository extends TypeOrmRepository<User> {
88
+ constructor(entityManager: EntityManager) {
89
+ super(User, entityManager);
110
90
  }
111
91
 
112
- async findByEmail(email: string): Promise<User | null> {
113
- return this.findOne({ where: { email } });
92
+ // Core methods available:
93
+ async findByEmail(email: string): Promise<User> {
94
+ return this.getOne({ where: { email } });
114
95
  }
115
96
 
116
97
  async findActiveUsers(): Promise<User[]> {
117
- return this.find({
118
- where: { isActive: true, deletedAt: null }
98
+ return this.getMany({
99
+ where: { isActive: true }
119
100
  });
120
101
  }
121
102
 
122
- async findUsersWithMetadata(key: string, value: any): Promise<User[]> {
123
- return this.createQueryBuilder('user')
124
- .where('user.metadata @> :metadata', {
125
- metadata: JSON.stringify({ [key]: value })
126
- })
127
- .getMany();
103
+ async findUsersWithCount(): Promise<[User[], number]> {
104
+ return this.getManyAndCount({
105
+ where: { isActive: true }
106
+ });
128
107
  }
129
108
 
130
- async softDelete(id: number): Promise<void> {
131
- await this.update(id, { deletedAt: new Date() });
109
+ async createUser(userData: Partial<User>): Promise<User> {
110
+ return this.postOne(userData);
132
111
  }
133
- }
134
- ```
135
-
136
- ## Configuration
137
-
138
- ### Data Source Configuration
139
-
140
- ```typescript
141
- import { dataSourceConfigFactory } from '@onivoro/server-typeorm-postgres';
142
112
 
143
- const config = dataSourceConfigFactory({
144
- host: process.env.DB_HOST,
145
- port: parseInt(process.env.DB_PORT),
146
- username: process.env.DB_USERNAME,
147
- password: process.env.DB_PASSWORD,
148
- database: process.env.DB_DATABASE,
149
- entities: [User, Product, Order],
150
- migrations: ['src/migrations/*.ts'],
151
- synchronize: false,
152
- logging: process.env.NODE_ENV === 'development',
153
- ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
154
- extra: {
155
- max: 20, // Connection pool size
156
- idleTimeoutMillis: 30000,
157
- connectionTimeoutMillis: 2000,
113
+ async createUsers(usersData: Partial<User>[]): Promise<User[]> {
114
+ return this.postMany(usersData);
158
115
  }
159
- });
160
- ```
161
-
162
- ### Dynamic Module Configuration
163
-
164
- ```typescript
165
- import { Module } from '@nestjs/common';
166
- import { ServerTypeormPostgresModule } from '@onivoro/server-typeorm-postgres';
167
- import { ConfigService } from '@nestjs/config';
168
-
169
- @Module({
170
- imports: [
171
- ServerTypeormPostgresModule.forRootAsync({
172
- useFactory: (configService: ConfigService) => ({
173
- host: configService.get('DATABASE_HOST'),
174
- port: configService.get('DATABASE_PORT'),
175
- username: configService.get('DATABASE_USERNAME'),
176
- password: configService.get('DATABASE_PASSWORD'),
177
- database: configService.get('DATABASE_NAME'),
178
- entities: [__dirname + '/**/*.entity{.ts,.js}'],
179
- migrations: [__dirname + '/migrations/*{.ts,.js}'],
180
- synchronize: configService.get('NODE_ENV') === 'development',
181
- logging: configService.get('DATABASE_LOGGING') === 'true',
182
- ssl: configService.get('NODE_ENV') === 'production' ? {
183
- rejectUnauthorized: false
184
- } : false
185
- }),
186
- inject: [ConfigService]
187
- })
188
- ],
189
- })
190
- export class DatabaseModule {}
191
- ```
192
116
 
193
- ## Usage Examples
194
-
195
- ### Migration Base Classes
117
+ async updateUser(id: number, updates: Partial<User>): Promise<void> {
118
+ // patch() uses TypeORM's update() method
119
+ await this.patch({ id }, updates);
120
+ }
196
121
 
197
- ```typescript
198
- import {
199
- TableMigrationBase,
200
- ColumnMigrationBase,
201
- IndexMigrationBase,
202
- DropTableMigrationBase
203
- } from '@onivoro/server-typeorm-postgres';
204
- import { MigrationInterface, QueryRunner } from 'typeorm';
122
+ async replaceUser(id: number, userData: Partial<User>): Promise<void> {
123
+ // put() uses TypeORM's save() method
124
+ await this.put({ id }, userData);
125
+ }
205
126
 
206
- export class CreateUsersTable1234567890 extends TableMigrationBase implements MigrationInterface {
207
- public async up(queryRunner: QueryRunner): Promise<void> {
208
- await this.createTable(queryRunner, 'users', [
209
- this.createColumn('id', 'SERIAL', { isPrimary: true }),
210
- this.createColumn('email', 'VARCHAR(255)', { isUnique: true, isNullable: false }),
211
- this.createColumn('first_name', 'VARCHAR(100)', { isNullable: false }),
212
- this.createColumn('last_name', 'VARCHAR(100)', { isNullable: false }),
213
- this.createColumn('metadata', 'JSONB', { default: "'{}'" }),
214
- this.createColumn('is_active', 'BOOLEAN', { default: true }),
215
- this.createColumn('created_at', 'TIMESTAMP', { default: 'CURRENT_TIMESTAMP' }),
216
- this.createColumn('updated_at', 'TIMESTAMP', { default: 'CURRENT_TIMESTAMP' }),
217
- this.createColumn('deleted_at', 'TIMESTAMP', { isNullable: true })
218
- ]);
127
+ async deleteUser(id: number): Promise<void> {
128
+ await this.delete({ id });
129
+ }
219
130
 
220
- // Add indexes
221
- await this.createIndex(queryRunner, 'users', ['email']);
222
- await this.createIndex(queryRunner, 'users', ['is_active']);
223
- await this.createIndex(queryRunner, 'users', ['created_at']);
131
+ async softDeleteUser(id: number): Promise<void> {
132
+ await this.softDelete({ id });
224
133
  }
225
134
 
226
- public async down(queryRunner: QueryRunner): Promise<void> {
227
- await this.dropTable(queryRunner, 'users');
135
+ // Transaction support
136
+ async createUserInTransaction(userData: Partial<User>, entityManager: EntityManager): Promise<User> {
137
+ const txRepository = this.forTransaction(entityManager);
138
+ return txRepository.postOne(userData);
228
139
  }
229
- }
230
140
 
231
- export class AddUserProfileColumns1234567891 extends ColumnMigrationBase implements MigrationInterface {
232
- public async up(queryRunner: QueryRunner): Promise<void> {
233
- await this.addColumn(queryRunner, 'users', 'phone_number', 'VARCHAR(20)', { isNullable: true });
234
- await this.addColumn(queryRunner, 'users', 'date_of_birth', 'DATE', { isNullable: true });
235
- await this.addColumn(queryRunner, 'users', 'avatar_url', 'TEXT', { isNullable: true });
141
+ // Custom queries with mapping
142
+ async findUsersByMetadata(key: string, value: any): Promise<User[]> {
143
+ const query = `
144
+ SELECT * FROM ${this.getTableNameExpression()}
145
+ WHERE metadata->>'${key}' = $1
146
+ AND deleted_at IS NULL
147
+ `;
148
+ return this.queryAndMap(query, [value]);
236
149
  }
237
150
 
238
- public async down(queryRunner: QueryRunner): Promise<void> {
239
- await this.dropColumn(queryRunner, 'users', 'avatar_url');
240
- await this.dropColumn(queryRunner, 'users', 'date_of_birth');
241
- await this.dropColumn(queryRunner, 'users', 'phone_number');
151
+ // Using ILike for case-insensitive search
152
+ async searchUsers(searchTerm: string): Promise<User[]> {
153
+ const filters = this.buildWhereILike({
154
+ firstName: searchTerm,
155
+ lastName: searchTerm,
156
+ email: searchTerm
157
+ });
158
+
159
+ return this.getMany({ where: filters });
242
160
  }
243
161
  }
244
162
  ```
245
163
 
246
- ### Advanced Repository Usage
164
+ ### TypeOrmPagingRepository
165
+
166
+ Abstract base class for implementing pagination:
247
167
 
248
168
  ```typescript
249
169
  import { Injectable } from '@nestjs/common';
250
- import { TypeOrmPagingRepository, PageParams, PagedData } from '@onivoro/server-typeorm-postgres';
170
+ import { TypeOrmPagingRepository, IPageParams, IPagedData } from '@onivoro/server-typeorm-postgres';
171
+ import { EntityManager, FindManyOptions } from 'typeorm';
251
172
  import { User } from './user.entity';
252
- import { FindOptionsWhere, ILike, Raw, Between } from 'typeorm';
253
173
 
254
- @Injectable()
255
- export class AdvancedUserRepository extends TypeOrmPagingRepository<User> {
256
- constructor() {
257
- super(User);
258
- }
174
+ // Define your custom params interface
175
+ interface UserPageParams {
176
+ isActive?: boolean;
177
+ search?: string;
178
+ departmentId?: number;
179
+ }
259
180
 
260
- async searchUsersWithFullText(
261
- searchTerm: string,
262
- pageParams: PageParams
263
- ): Promise<PagedData<User>> {
264
- // PostgreSQL full-text search
265
- return this.findWithPaging(
266
- {
267
- where: Raw(alias => `to_tsvector('english', ${alias}.first_name || ' ' || ${alias}.last_name || ' ' || ${alias}.email) @@ plainto_tsquery('english', :searchTerm)`, { searchTerm }),
268
- order: { createdAt: 'DESC' }
269
- },
270
- pageParams
271
- );
181
+ @Injectable()
182
+ export class UserPagingRepository extends TypeOrmPagingRepository<User, UserPageParams> {
183
+ constructor(entityManager: EntityManager) {
184
+ super(User, entityManager);
272
185
  }
273
186
 
274
- async findUsersByMetadataPath(
275
- jsonPath: string,
276
- value: any,
277
- pageParams: PageParams
278
- ): Promise<PagedData<User>> {
279
- return this.findWithPaging(
280
- {
281
- where: Raw(alias => `${alias}.metadata #>> :path = :value`, {
282
- path: `{${jsonPath}}`,
283
- value: String(value)
284
- })
285
- },
286
- pageParams
287
- );
288
- }
187
+ // You must implement the abstract getPage method
188
+ async getPage(pageParams: IPageParams, params: UserPageParams): Promise<IPagedData<User>> {
189
+ const { page, limit } = pageParams;
190
+ const skip = this.getSkip(page, limit);
289
191
 
290
- async findUsersWithArrayContains(
291
- metadataKey: string,
292
- containsValue: string
293
- ): Promise<User[]> {
294
- return this.createQueryBuilder('user')
295
- .where(`user.metadata->:key @> :value`, {
296
- key: metadataKey,
297
- value: JSON.stringify([containsValue])
298
- })
299
- .getMany();
300
- }
192
+ // Build where conditions
193
+ const where = this.removeFalseyKeys({
194
+ isActive: params.isActive,
195
+ departmentId: params.departmentId
196
+ });
301
197
 
302
- async findUsersByDateRange(
303
- startDate: Date,
304
- endDate: Date,
305
- pageParams: PageParams
306
- ): Promise<PagedData<User>> {
307
- return this.findWithPaging(
308
- {
309
- where: {
310
- createdAt: Between(startDate, endDate),
311
- deletedAt: null
312
- },
313
- order: { createdAt: 'DESC' }
314
- },
315
- pageParams
316
- );
317
- }
198
+ // Add search conditions if provided
199
+ if (params.search) {
200
+ Object.assign(where, this.buildWhereILike({
201
+ firstName: params.search,
202
+ lastName: params.search,
203
+ email: params.search
204
+ }));
205
+ }
318
206
 
319
- async getUserAggregateStats(): Promise<{
320
- total: number;
321
- active: number;
322
- inactive: number;
323
- avgMetadataSize: number;
324
- recentRegistrations: number;
325
- }> {
326
- const result = await this.createQueryBuilder('user')
327
- .select([
328
- 'COUNT(*) as total',
329
- 'COUNT(CASE WHEN user.isActive = true THEN 1 END) as active',
330
- 'COUNT(CASE WHEN user.isActive = false THEN 1 END) as inactive',
331
- 'AVG(jsonb_array_length(user.metadata)) as avgMetadataSize',
332
- `COUNT(CASE WHEN user.createdAt >= :weekAgo THEN 1 END) as recentRegistrations`
333
- ])
334
- .where('user.deletedAt IS NULL')
335
- .setParameter('weekAgo', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
336
- .getRawOne();
207
+ const [data, total] = await this.getManyAndCount({
208
+ where,
209
+ skip,
210
+ take: limit,
211
+ order: { createdAt: 'DESC' }
212
+ });
337
213
 
338
214
  return {
339
- total: parseInt(result.total),
340
- active: parseInt(result.active),
341
- inactive: parseInt(result.inactive),
342
- avgMetadataSize: parseFloat(result.avgmetadatasize) || 0,
343
- recentRegistrations: parseInt(result.recentregistrations)
215
+ data,
216
+ total,
217
+ page,
218
+ limit,
219
+ totalPages: Math.ceil(total / limit),
220
+ hasNext: page < Math.ceil(total / limit),
221
+ hasPrev: page > 1
344
222
  };
345
223
  }
224
+
225
+ // You can add additional helper methods
226
+ getCacheKey(pageParams: IPageParams, params: UserPageParams): string {
227
+ return this.getPagingKey(pageParams.page, pageParams.limit) + '_' + JSON.stringify(params);
228
+ }
346
229
  }
347
230
  ```
348
231
 
349
- ### Redshift Integration
232
+ ### RedshiftRepository
233
+
234
+ Specialized repository for Amazon Redshift with custom SQL building:
350
235
 
351
236
  ```typescript
352
237
  import { Injectable } from '@nestjs/common';
353
238
  import { RedshiftRepository } from '@onivoro/server-typeorm-postgres';
239
+ import { EntityManager } from 'typeorm';
240
+ import { AnalyticsEvent } from './analytics-event.entity';
354
241
 
355
242
  @Injectable()
356
- export class AnalyticsRepository extends RedshiftRepository {
357
- constructor() {
358
- super();
243
+ export class AnalyticsRepository extends RedshiftRepository<AnalyticsEvent> {
244
+ constructor(entityManager: EntityManager) {
245
+ super(AnalyticsEvent, entityManager);
359
246
  }
360
247
 
361
- async getUserActivitySummary(startDate: Date, endDate: Date) {
362
- return this.query(`
363
- SELECT
364
- u.id,
365
- u.email,
366
- COUNT(a.id) as activity_count,
367
- MAX(a.created_at) as last_activity,
368
- AVG(a.duration) as avg_duration
369
- FROM users u
370
- LEFT JOIN user_activities a ON u.id = a.user_id
371
- WHERE a.created_at BETWEEN $1 AND $2
372
- GROUP BY u.id, u.email
373
- ORDER BY activity_count DESC
374
- LIMIT 100
375
- `, [startDate, endDate]);
248
+ // RedshiftRepository overrides several methods for Redshift compatibility
249
+ async createAnalyticsEvent(event: Partial<AnalyticsEvent>): Promise<AnalyticsEvent> {
250
+ // Uses custom SQL building and retrieval
251
+ return this.postOne(event);
376
252
  }
377
253
 
378
- async getMonthlyUserGrowth() {
379
- return this.query(`
380
- SELECT
381
- DATE_TRUNC('month', created_at) as month,
382
- COUNT(*) as new_users,
383
- SUM(COUNT(*)) OVER (ORDER BY DATE_TRUNC('month', created_at)) as cumulative_users
384
- FROM users
385
- WHERE deleted_at IS NULL
386
- GROUP BY DATE_TRUNC('month', created_at)
387
- ORDER BY month
388
- `);
254
+ async bulkInsertEvents(events: Partial<AnalyticsEvent>[]): Promise<AnalyticsEvent[]> {
255
+ // Uses optimized bulk insert
256
+ return this.postMany(events);
389
257
  }
390
258
 
391
- async getUserSegmentAnalysis() {
392
- return this.query(`
393
- WITH user_segments AS (
394
- SELECT
395
- u.id,
396
- u.metadata->>'segment' as segment,
397
- COUNT(o.id) as order_count,
398
- SUM(o.total_amount) as total_spent
399
- FROM users u
400
- LEFT JOIN orders o ON u.id = o.user_id
401
- WHERE u.deleted_at IS NULL
402
- GROUP BY u.id, u.metadata->>'segment'
403
- )
259
+ // Performance-optimized methods unique to RedshiftRepository
260
+ async insertWithoutReturn(event: Partial<AnalyticsEvent>): Promise<void> {
261
+ // Inserts without performing retrieval query
262
+ await this.postOneWithoutReturn(event);
263
+ }
264
+
265
+ async bulkInsertWithoutReturn(events: Partial<AnalyticsEvent>[]): Promise<void> {
266
+ // NOTE: Currently throws NotImplementedException
267
+ // await this.postManyWithoutReturn(events);
268
+ }
269
+
270
+ // Custom analytics queries
271
+ async getEventAnalytics(startDate: Date, endDate: Date) {
272
+ const query = `
404
273
  SELECT
405
- segment,
406
- COUNT(*) as user_count,
407
- AVG(order_count) as avg_orders_per_user,
408
- AVG(total_spent) as avg_spend_per_user,
409
- SUM(total_spent) as total_segment_revenue
410
- FROM user_segments
411
- GROUP BY segment
412
- ORDER BY total_segment_revenue DESC
413
- `);
274
+ event_type,
275
+ COUNT(*) as event_count,
276
+ COUNT(DISTINCT user_id) as unique_users,
277
+ DATE_TRUNC('day', created_at) as event_date
278
+ FROM ${this.getTableNameExpression()}
279
+ WHERE created_at BETWEEN $1 AND $2
280
+ GROUP BY event_type, event_date
281
+ ORDER BY event_date DESC, event_count DESC
282
+ `;
283
+
284
+ return this.query(query, [startDate, endDate]);
285
+ }
286
+
287
+ // Redshift handles JSONB differently
288
+ async findEventsByJsonData(key: string, value: any): Promise<AnalyticsEvent[]> {
289
+ // JSON_PARSE is used automatically for jsonb columns in Redshift
290
+ const query = `
291
+ SELECT * FROM ${this.getTableNameExpression()}
292
+ WHERE JSON_EXTRACT_PATH_TEXT(event_data, '${key}') = $1
293
+ `;
294
+
295
+ return this.queryAndMap(query, [value]);
414
296
  }
415
297
  }
416
298
  ```
417
299
 
418
- ### SQL Writer Utilities
300
+ ## SQL Writer
301
+
302
+ Static utility class for generating PostgreSQL DDL:
419
303
 
420
304
  ```typescript
421
- import { Injectable } from '@nestjs/common';
422
305
  import { SqlWriter } from '@onivoro/server-typeorm-postgres';
423
- import { DataSource } from 'typeorm';
424
-
425
- @Injectable()
426
- export class ReportingService {
427
- private sqlWriter: SqlWriter;
428
-
429
- constructor(private dataSource: DataSource) {
430
- this.sqlWriter = new SqlWriter(dataSource);
431
- }
432
-
433
- async generateUserReport(filters: {
434
- startDate?: Date;
435
- endDate?: Date;
436
- segment?: string;
437
- isActive?: boolean;
438
- }) {
439
- const query = this.sqlWriter
440
- .select([
441
- 'u.id',
442
- 'u.email',
443
- 'u.first_name',
444
- 'u.last_name',
445
- 'u.metadata',
446
- 'u.created_at',
447
- 'COUNT(o.id) as order_count',
448
- 'SUM(o.total_amount) as total_spent'
449
- ])
450
- .from('users', 'u')
451
- .leftJoin('orders', 'o', 'u.id = o.user_id')
452
- .where('u.deleted_at IS NULL');
453
-
454
- if (filters.startDate) {
455
- query.andWhere('u.created_at >= :startDate', { startDate: filters.startDate });
456
- }
306
+ import { TableColumnOptions } from 'typeorm';
307
+
308
+ // Add single column
309
+ const addColumnSql = SqlWriter.addColumn('users', {
310
+ name: 'phone_number',
311
+ type: 'varchar',
312
+ length: 20,
313
+ isNullable: true
314
+ });
315
+ // Returns: ALTER TABLE "users" ADD "phone_number" varchar(20)
316
+
317
+ // Create table with multiple columns
318
+ const createTableSql = SqlWriter.createTable('products', [
319
+ { name: 'id', type: 'serial', isPrimary: true },
320
+ { name: 'name', type: 'varchar', length: 255, isNullable: false },
321
+ { name: 'price', type: 'decimal', precision: 10, scale: 2 },
322
+ { name: 'metadata', type: 'jsonb', default: {} },
323
+ { name: 'created_at', type: 'timestamp', default: 'CURRENT_TIMESTAMP' }
324
+ ]);
325
+
326
+ // Drop table
327
+ const dropTableSql = SqlWriter.dropTable('products');
328
+ // Returns: DROP TABLE "products";
329
+
330
+ // Add multiple columns
331
+ const addColumnsSql = SqlWriter.addColumns('products', [
332
+ { name: 'category_id', type: 'int', isNullable: true },
333
+ { name: 'sku', type: 'varchar', length: 50, isUnique: true }
334
+ ]);
335
+
336
+ // Drop column
337
+ const dropColumnSql = SqlWriter.dropColumn('products', { name: 'old_column' });
338
+ // Returns: ALTER TABLE "products" DROP COLUMN old_column
339
+
340
+ // Create indexes
341
+ const createIndexSql = SqlWriter.createIndex('products', 'name', false);
342
+ // Returns: CREATE INDEX IF NOT EXISTS products_name ON "products"(name)
343
+
344
+ const createUniqueIndexSql = SqlWriter.createUniqueIndex('products', 'sku');
345
+ // Returns: CREATE UNIQUE INDEX IF NOT EXISTS products_sku ON "products"(sku)
346
+
347
+ // Drop index
348
+ const dropIndexSql = SqlWriter.dropIndex('products_name');
349
+ // Returns: DROP INDEX IF EXISTS products_name
350
+
351
+ // Handle special default values
352
+ const jsonbColumn: TableColumnOptions = {
353
+ name: 'settings',
354
+ type: 'jsonb',
355
+ default: { notifications: true, theme: 'light' }
356
+ };
357
+ const jsonbSql = SqlWriter.addColumn('users', jsonbColumn);
358
+ // Returns: ALTER TABLE "users" ADD "settings" jsonb DEFAULT '{"notifications":true,"theme":"light"}'::jsonb
359
+
360
+ // Boolean and numeric defaults
361
+ const booleanSql = SqlWriter.addColumn('users', {
362
+ name: 'is_verified',
363
+ type: 'boolean',
364
+ default: false
365
+ });
366
+ // Returns: ALTER TABLE "users" ADD "is_verified" boolean DEFAULT FALSE
367
+ ```
457
368
 
458
- if (filters.endDate) {
459
- query.andWhere('u.created_at <= :endDate', { endDate: filters.endDate });
460
- }
369
+ ## Migration Base Classes
461
370
 
462
- if (filters.segment) {
463
- query.andWhere("u.metadata->>'segment' = :segment", { segment: filters.segment });
464
- }
371
+ ### TableMigrationBase
465
372
 
466
- if (filters.isActive !== undefined) {
467
- query.andWhere('u.is_active = :isActive', { isActive: filters.isActive });
468
- }
373
+ ```typescript
374
+ import { TableMigrationBase } from '@onivoro/server-typeorm-postgres';
375
+ import { MigrationInterface } from 'typeorm';
469
376
 
470
- return query
471
- .groupBy(['u.id', 'u.email', 'u.first_name', 'u.last_name', 'u.metadata', 'u.created_at'])
472
- .orderBy('u.created_at', 'DESC')
473
- .execute();
377
+ export class CreateUsersTable1234567890 extends TableMigrationBase implements MigrationInterface {
378
+ constructor() {
379
+ super('users', [
380
+ { name: 'id', type: 'serial', isPrimary: true },
381
+ { name: 'email', type: 'varchar', length: 255, isUnique: true, isNullable: false },
382
+ { name: 'first_name', type: 'varchar', length: 100, isNullable: false },
383
+ { name: 'metadata', type: 'jsonb', default: '{}' },
384
+ { name: 'is_active', type: 'boolean', default: true },
385
+ { name: 'created_at', type: 'timestamp', default: 'CURRENT_TIMESTAMP' },
386
+ { name: 'deleted_at', type: 'timestamp', isNullable: true }
387
+ ]);
474
388
  }
389
+ }
390
+ ```
475
391
 
476
- async generateDashboardMetrics() {
477
- const queries = {
478
- totalUsers: this.sqlWriter
479
- .select('COUNT(*)')
480
- .from('users')
481
- .where('deleted_at IS NULL'),
482
-
483
- activeUsers: this.sqlWriter
484
- .select('COUNT(*)')
485
- .from('users')
486
- .where('deleted_at IS NULL')
487
- .andWhere('is_active = true'),
488
-
489
- newUsersThisMonth: this.sqlWriter
490
- .select('COUNT(*)')
491
- .from('users')
492
- .where('deleted_at IS NULL')
493
- .andWhere("created_at >= DATE_TRUNC('month', CURRENT_DATE)"),
494
-
495
- totalOrders: this.sqlWriter
496
- .select('COUNT(*)')
497
- .from('orders'),
498
-
499
- totalRevenue: this.sqlWriter
500
- .select('SUM(total_amount)')
501
- .from('orders')
502
- .where("status != 'cancelled'")
503
- };
392
+ ### ColumnMigrationBase
504
393
 
505
- const results = await Promise.all(
506
- Object.entries(queries).map(async ([key, query]) => [
507
- key,
508
- await query.getRawOne()
509
- ])
510
- );
394
+ ```typescript
395
+ import { ColumnMigrationBase } from '@onivoro/server-typeorm-postgres';
511
396
 
512
- return Object.fromEntries(results);
397
+ export class AddUserPhoneNumber1234567891 extends ColumnMigrationBase {
398
+ constructor() {
399
+ super('users', {
400
+ name: 'phone_number',
401
+ type: 'varchar',
402
+ length: 20,
403
+ isNullable: true
404
+ });
513
405
  }
514
406
  }
515
407
  ```
516
408
 
517
- ### Complex Entity Relationships
409
+ ### ColumnsMigrationBase
518
410
 
519
411
  ```typescript
520
- import {
521
- Table,
522
- PrimaryTableColumn,
523
- TableColumn,
524
- NullableTableColumn,
525
- ManyToOneRelationOptions
526
- } from '@onivoro/server-typeorm-postgres';
527
- import { Entity, ManyToOne, OneToMany, JoinColumn, Index } from 'typeorm';
528
-
529
- @Entity()
530
- @Table('orders')
531
- @Index(['userId', 'status'])
532
- @Index(['createdAt'])
533
- export class Order {
534
- @PrimaryTableColumn()
535
- id: number;
412
+ import { ColumnsMigrationBase } from '@onivoro/server-typeorm-postgres';
536
413
 
537
- @TableColumn({ type: 'varchar', length: 50, unique: true })
538
- orderNumber: string;
539
-
540
- @TableColumn({ type: 'decimal', precision: 12, scale: 2 })
541
- totalAmount: number;
542
-
543
- @TableColumn({
544
- type: 'enum',
545
- enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded']
546
- })
547
- status: string;
548
-
549
- @TableColumn({ type: 'int' })
550
- userId: number;
551
-
552
- @TableColumn({ type: 'jsonb', default: '{}' })
553
- metadata: Record<string, any>;
554
-
555
- @TableColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
556
- createdAt: Date;
557
-
558
- @NullableTableColumn({ type: 'timestamp' })
559
- shippedAt?: Date;
560
-
561
- @NullableTableColumn({ type: 'timestamp' })
562
- deliveredAt?: Date;
563
-
564
- @NullableTableColumn({ type: 'timestamp' })
565
- cancelledAt?: Date;
566
-
567
- // Full-text search column
568
- @TableColumn({ type: 'tsvector', select: false })
569
- searchVector: string;
570
-
571
- // Relationships
572
- @ManyToOne(() => User, user => user.orders, ManyToOneRelationOptions)
573
- @JoinColumn({ name: 'userId' })
574
- user: User;
575
-
576
- @OneToMany(() => OrderItem, orderItem => orderItem.order, { cascade: true })
577
- items: OrderItem[];
414
+ export class AddUserContactInfo1234567892 extends ColumnsMigrationBase {
415
+ constructor() {
416
+ super('users', [
417
+ { name: 'phone_number', type: 'varchar', length: 20, isNullable: true },
418
+ { name: 'secondary_email', type: 'varchar', length: 255, isNullable: true },
419
+ { name: 'address', type: 'jsonb', isNullable: true }
420
+ ]);
421
+ }
578
422
  }
423
+ ```
579
424
 
580
- @Entity()
581
- @Table('order_items')
582
- @Index(['orderId', 'productId'])
583
- export class OrderItem {
584
- @PrimaryTableColumn()
585
- id: number;
586
-
587
- @TableColumn({ type: 'int' })
588
- orderId: number;
589
-
590
- @TableColumn({ type: 'int' })
591
- productId: number;
425
+ ### IndexMigrationBase
592
426
 
593
- @TableColumn({ type: 'int' })
594
- quantity: number;
427
+ ```typescript
428
+ import { IndexMigrationBase } from '@onivoro/server-typeorm-postgres';
595
429
 
596
- @TableColumn({ type: 'decimal', precision: 10, scale: 2 })
597
- unitPrice: number;
430
+ export class CreateUserEmailIndex1234567893 extends IndexMigrationBase {
431
+ constructor() {
432
+ super('users', 'email', true); // table, column, unique
433
+ }
434
+ }
435
+ ```
598
436
 
599
- @TableColumn({ type: 'decimal', precision: 10, scale: 2 })
600
- totalPrice: number;
437
+ ### DropTableMigrationBase & DropColumnMigrationBase
601
438
 
602
- @TableColumn({ type: 'jsonb', default: '{}' })
603
- productSnapshot: Record<string, any>;
439
+ ```typescript
440
+ import { DropTableMigrationBase, DropColumnMigrationBase } from '@onivoro/server-typeorm-postgres';
604
441
 
605
- @ManyToOne(() => Order, order => order.items, ManyToOneRelationOptions)
606
- @JoinColumn({ name: 'orderId' })
607
- order: Order;
442
+ export class DropLegacyUsersTable1234567894 extends DropTableMigrationBase {
443
+ constructor() {
444
+ super('legacy_users');
445
+ }
446
+ }
608
447
 
609
- @ManyToOne(() => Product, product => product.orderItems, ManyToOneRelationOptions)
610
- @JoinColumn({ name: 'productId' })
611
- product: Product;
448
+ export class DropUserMiddleName1234567895 extends DropColumnMigrationBase {
449
+ constructor() {
450
+ super('users', { name: 'middle_name' });
451
+ }
612
452
  }
613
453
  ```
614
454
 
615
- ### Advanced PostgreSQL Features
455
+ ## Building Repositories from Metadata
456
+
457
+ Both TypeOrmRepository and RedshiftRepository support building instances from metadata:
616
458
 
617
459
  ```typescript
618
- import { Injectable } from '@nestjs/common';
460
+ import { TypeOrmRepository, RedshiftRepository } from '@onivoro/server-typeorm-postgres';
619
461
  import { DataSource } from 'typeorm';
620
462
 
621
- @Injectable()
622
- export class PostgresAdvancedService {
623
- constructor(private dataSource: DataSource) {}
624
-
625
- async createFullTextSearchIndex(tableName: string, columns: string[]): Promise<void> {
626
- const vectorColumn = `${tableName}_search_vector`;
627
- const indexName = `idx_${tableName}_fulltext`;
628
-
629
- // Add tsvector column if it doesn't exist
630
- await this.dataSource.query(`
631
- ALTER TABLE ${tableName}
632
- ADD COLUMN IF NOT EXISTS ${vectorColumn} tsvector
633
- `);
634
-
635
- // Create trigger to update search vector
636
- await this.dataSource.query(`
637
- CREATE OR REPLACE FUNCTION update_${tableName}_search_vector()
638
- RETURNS trigger AS $$
639
- BEGIN
640
- NEW.${vectorColumn} := to_tsvector('english', ${columns.map(col => `COALESCE(NEW.${col}, '')`).join(" || ' ' || ")});
641
- RETURN NEW;
642
- END;
643
- $$ LANGUAGE plpgsql;
644
- `);
645
-
646
- // Create trigger
647
- await this.dataSource.query(`
648
- DROP TRIGGER IF EXISTS trigger_${tableName}_search_vector ON ${tableName};
649
- CREATE TRIGGER trigger_${tableName}_search_vector
650
- BEFORE INSERT OR UPDATE ON ${tableName}
651
- FOR EACH ROW EXECUTE FUNCTION update_${tableName}_search_vector();
652
- `);
653
-
654
- // Create GIN index
655
- await this.dataSource.query(`
656
- CREATE INDEX IF NOT EXISTS ${indexName}
657
- ON ${tableName} USING gin(${vectorColumn})
658
- `);
659
-
660
- // Update existing records
661
- await this.dataSource.query(`
662
- UPDATE ${tableName}
663
- SET ${vectorColumn} = to_tsvector('english', ${columns.map(col => `COALESCE(${col}, '')`).join(" || ' ' || ")})
664
- `);
665
- }
666
-
667
- async performFullTextSearch(
668
- tableName: string,
669
- searchTerm: string,
670
- limit: number = 10
671
- ): Promise<any[]> {
672
- const vectorColumn = `${tableName}_search_vector`;
673
-
674
- return this.dataSource.query(`
675
- SELECT *,
676
- ts_rank(${vectorColumn}, plainto_tsquery('english', $1)) as rank
677
- FROM ${tableName}
678
- WHERE ${vectorColumn} @@ plainto_tsquery('english', $1)
679
- ORDER BY rank DESC
680
- LIMIT $2
681
- `, [searchTerm, limit]);
682
- }
683
-
684
- async createPartitionedTable(
685
- tableName: string,
686
- partitionColumn: string,
687
- partitionType: 'RANGE' | 'LIST' | 'HASH' = 'RANGE'
688
- ): Promise<void> {
689
- await this.dataSource.query(`
690
- CREATE TABLE ${tableName}_partitioned (
691
- LIKE ${tableName} INCLUDING ALL
692
- ) PARTITION BY ${partitionType} (${partitionColumn})
693
- `);
694
- }
463
+ // Define your entity type
464
+ interface UserEvent {
465
+ id: number;
466
+ userId: number;
467
+ eventType: string;
468
+ eventData: any;
469
+ createdAt: Date;
470
+ }
695
471
 
696
- async createMonthlyPartitions(
697
- tableName: string,
698
- startDate: Date,
699
- months: number
700
- ): Promise<void> {
701
- for (let i = 0; i < months; i++) {
702
- const date = new Date(startDate);
703
- date.setMonth(date.getMonth() + i);
704
-
705
- const year = date.getFullYear();
706
- const month = String(date.getMonth() + 1).padStart(2, '0');
707
- const partitionName = `${tableName}_${year}_${month}`;
708
-
709
- const startOfMonth = new Date(year, date.getMonth(), 1);
710
- const startOfNextMonth = new Date(year, date.getMonth() + 1, 1);
711
-
712
- await this.dataSource.query(`
713
- CREATE TABLE IF NOT EXISTS ${partitionName}
714
- PARTITION OF ${tableName}_partitioned
715
- FOR VALUES FROM ('${startOfMonth.toISOString()}') TO ('${startOfNextMonth.toISOString()}')
716
- `);
472
+ // Build TypeORM repository from metadata
473
+ const userEventRepo = TypeOrmRepository.buildFromMetadata<UserEvent>(dataSource, {
474
+ schema: 'public',
475
+ table: 'user_events',
476
+ columns: {
477
+ id: {
478
+ databasePath: 'id',
479
+ type: 'int',
480
+ propertyPath: 'id',
481
+ isPrimary: true,
482
+ default: undefined
483
+ },
484
+ userId: {
485
+ databasePath: 'user_id',
486
+ type: 'int',
487
+ propertyPath: 'userId',
488
+ isPrimary: false,
489
+ default: undefined
490
+ },
491
+ eventType: {
492
+ databasePath: 'event_type',
493
+ type: 'varchar',
494
+ propertyPath: 'eventType',
495
+ isPrimary: false,
496
+ default: undefined
497
+ },
498
+ eventData: {
499
+ databasePath: 'event_data',
500
+ type: 'jsonb',
501
+ propertyPath: 'eventData',
502
+ isPrimary: false,
503
+ default: {}
504
+ },
505
+ createdAt: {
506
+ databasePath: 'created_at',
507
+ type: 'timestamp',
508
+ propertyPath: 'createdAt',
509
+ isPrimary: false,
510
+ default: 'CURRENT_TIMESTAMP'
717
511
  }
718
512
  }
513
+ });
719
514
 
720
- async createHypertable(tableName: string, timeColumn: string): Promise<void> {
721
- // For TimescaleDB extension
722
- await this.dataSource.query(`
723
- SELECT create_hypertable('${tableName}', '${timeColumn}', if_not_exists => TRUE)
724
- `);
725
- }
726
-
727
- async analyzeTableStatistics(tableName: string): Promise<any> {
728
- return this.dataSource.query(`
729
- SELECT
730
- schemaname,
731
- tablename,
732
- attname,
733
- n_distinct,
734
- most_common_vals,
735
- most_common_freqs,
736
- histogram_bounds
737
- FROM pg_stats
738
- WHERE tablename = $1
739
- `, [tableName]);
515
+ // Build Redshift repository from metadata
516
+ const analyticsRepo = RedshiftRepository.buildFromMetadata<UserEvent>(redshiftDataSource, {
517
+ schema: 'analytics',
518
+ table: 'user_events',
519
+ columns: {
520
+ // Same column definitions as above
740
521
  }
522
+ });
741
523
 
742
- async getTableSize(tableName: string): Promise<any> {
743
- return this.dataSource.query(`
744
- SELECT
745
- pg_size_pretty(pg_total_relation_size($1)) as total_size,
746
- pg_size_pretty(pg_relation_size($1)) as table_size,
747
- pg_size_pretty(pg_indexes_size($1)) as indexes_size
748
- `, [tableName]);
749
- }
750
- }
524
+ // Use the repositories
525
+ const events = await userEventRepo.getMany({ where: { userId: 123 } });
526
+ const recentEvent = await userEventRepo.getOne({ where: { id: 456 } });
751
527
  ```
752
528
 
753
- ## API Reference
754
-
755
- ### Repository Classes
756
-
757
- #### TypeOrmRepository<T>
758
-
759
- Base repository class with PostgreSQL optimizations:
529
+ ## Data Source Configuration
760
530
 
761
531
  ```typescript
762
- export class TypeOrmRepository<T> extends Repository<T> {
763
- constructor(entity: EntityTarget<T>)
764
- }
765
- ```
766
-
767
- #### TypeOrmPagingRepository<T>
768
-
769
- Repository with built-in pagination support:
532
+ import { dataSourceFactory, dataSourceConfigFactory } from '@onivoro/server-typeorm-postgres';
533
+ import { User, Product, Order } from './entities';
534
+
535
+ // Using data source factory
536
+ const dataSource = dataSourceFactory('postgres-main', {
537
+ host: 'localhost',
538
+ port: 5432,
539
+ username: 'postgres',
540
+ password: 'password',
541
+ database: 'myapp'
542
+ }, [User, Product, Order]);
543
+
544
+ // Using config factory for more control
545
+ const config = dataSourceConfigFactory('postgres-main', {
546
+ host: process.env.DB_HOST,
547
+ port: parseInt(process.env.DB_PORT),
548
+ username: process.env.DB_USERNAME,
549
+ password: process.env.DB_PASSWORD,
550
+ database: process.env.DB_DATABASE,
551
+ ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
552
+ }, [User, Product, Order]);
770
553
 
771
- ```typescript
772
- export class TypeOrmPagingRepository<T> extends TypeOrmRepository<T> {
773
- async findWithPaging(
774
- options: FindManyOptions<T>,
775
- pageParams: PageParams
776
- ): Promise<PagedData<T>>
777
- }
554
+ const dataSource = new DataSource(config);
778
555
  ```
779
556
 
780
- #### RedshiftRepository
557
+ ## Type Definitions
781
558
 
782
- Repository for Amazon Redshift operations:
559
+ ### Core Types
783
560
 
784
561
  ```typescript
785
- export class RedshiftRepository {
786
- async query(sql: string, parameters?: any[]): Promise<any[]>
787
- async execute(sql: string, parameters?: any[]): Promise<void>
562
+ // Table metadata
563
+ interface TTableMeta {
564
+ databasePath: string;
565
+ type: string;
566
+ propertyPath: string;
567
+ isPrimary: boolean;
568
+ default?: any;
788
569
  }
789
- ```
790
-
791
- ### Migration Base Classes
792
570
 
793
- #### TableMigrationBase
794
-
795
- Base class for table creation migrations:
796
-
797
- ```typescript
798
- export abstract class TableMigrationBase {
799
- protected createTable(queryRunner: QueryRunner, tableName: string, columns: ColumnDefinition[]): Promise<void>
800
- protected dropTable(queryRunner: QueryRunner, tableName: string): Promise<void>
801
- protected createIndex(queryRunner: QueryRunner, tableName: string, columns: string[]): Promise<void>
571
+ // Page parameters
572
+ interface IPageParams {
573
+ page: number;
574
+ limit: number;
802
575
  }
803
- ```
804
576
 
805
- #### ColumnMigrationBase
577
+ // Paged data result
578
+ interface IPagedData<T> {
579
+ data: T[];
580
+ total: number;
581
+ page: number;
582
+ limit: number;
583
+ totalPages: number;
584
+ hasNext: boolean;
585
+ hasPrev: boolean;
586
+ }
806
587
 
807
- Base class for column modifications:
588
+ // Data source options
589
+ interface IDataSourceOptions {
590
+ host: string;
591
+ port: number;
592
+ username: string;
593
+ password: string;
594
+ database: string;
595
+ ssl?: any;
596
+ extra?: any;
597
+ }
808
598
 
809
- ```typescript
810
- export abstract class ColumnMigrationBase {
811
- protected addColumn(queryRunner: QueryRunner, tableName: string, columnName: string, type: string, options?: ColumnOptions): Promise<void>
812
- protected dropColumn(queryRunner: QueryRunner, tableName: string, columnName: string): Promise<void>
813
- protected changeColumn(queryRunner: QueryRunner, tableName: string, columnName: string, newType: string): Promise<void>
599
+ // Entity provider interface
600
+ interface IEntityProvider<TEntity, TFindOneOptions, TFindManyOptions, TFindOptionsWhere, TUpdateData> {
601
+ getOne(options: TFindOneOptions): Promise<TEntity>;
602
+ getMany(options: TFindManyOptions): Promise<TEntity[]>;
603
+ getManyAndCount(options: TFindManyOptions): Promise<[TEntity[], number]>;
604
+ postOne(body: Partial<TEntity>): Promise<TEntity>;
605
+ postMany(body: Partial<TEntity>[]): Promise<TEntity[]>;
606
+ delete(options: TFindOptionsWhere): Promise<void>;
607
+ softDelete(options: TFindOptionsWhere): Promise<void>;
608
+ put(options: TFindOptionsWhere, body: TUpdateData): Promise<void>;
609
+ patch(options: TFindOptionsWhere, body: TUpdateData): Promise<void>;
814
610
  }
815
611
  ```
816
612
 
817
- ### SQL Writer
818
-
819
- #### SqlWriter
820
-
821
- Advanced SQL query builder:
613
+ ## Utility Functions
822
614
 
823
615
  ```typescript
824
- export class SqlWriter {
825
- constructor(dataSource: DataSource)
826
-
827
- select(columns: string[]): SqlWriter
828
- from(table: string, alias?: string): SqlWriter
829
- leftJoin(table: string, alias: string, condition: string): SqlWriter
830
- where(condition: string, parameters?: Record<string, any>): SqlWriter
831
- groupBy(columns: string[]): SqlWriter
832
- orderBy(column: string, direction?: 'ASC' | 'DESC'): SqlWriter
833
- execute(): Promise<any[]>
834
- }
835
- ```
616
+ import {
617
+ getSkip,
618
+ getPagingKey,
619
+ removeFalseyKeys,
620
+ generateDateQuery,
621
+ getApiTypeFromColumn
622
+ } from '@onivoro/server-typeorm-postgres';
836
623
 
837
- ### Type Definitions
624
+ // Calculate skip value for pagination
625
+ const skip = getSkip(2, 20); // page 2, limit 20 = skip 20
838
626
 
839
- #### TableMeta
627
+ // Generate cache key for pagination
628
+ const cacheKey = getPagingKey(2, 20); // Returns: "page_2_limit_20"
840
629
 
841
- Table metadata type:
630
+ // Remove falsey values from object
631
+ const cleanedFilters = removeFalseyKeys({
632
+ name: 'John',
633
+ age: 0, // Removed
634
+ active: false, // Kept (false is not falsey for this function)
635
+ email: '', // Removed
636
+ dept: null // Removed
637
+ });
842
638
 
843
- ```typescript
844
- interface TableMeta {
845
- name: string;
846
- schema?: string;
847
- columns: ColumnMeta[];
848
- indexes: IndexMeta[];
849
- }
639
+ // Generate date range query
640
+ const dateQuery = generateDateQuery('created_at', {
641
+ startDate: new Date('2024-01-01'),
642
+ endDate: new Date('2024-12-31')
643
+ });
644
+
645
+ // Get API type from TypeORM column metadata
646
+ const apiType = getApiTypeFromColumn(columnMetadata);
850
647
  ```
851
648
 
852
649
  ## Best Practices
853
650
 
854
- 1. **Use Indexes Wisely**: Create appropriate indexes for query performance
855
- 2. **Leverage JSONB**: Use JSONB for flexible schema requirements
856
- 3. **Partition Large Tables**: Use table partitioning for time-series data
857
- 4. **Full-Text Search**: Implement PostgreSQL full-text search for text queries
858
- 5. **Connection Pooling**: Configure proper connection pooling
859
- 6. **Migration Strategy**: Use structured migration classes
860
- 7. **Monitor Performance**: Use PostgreSQL statistics for performance monitoring
861
- 8. **Backup Strategy**: Implement regular backup procedures
862
-
863
- ## Performance Optimization
864
-
865
- ```typescript
866
- // Example of optimized queries
867
- const optimizedQuery = repository
868
- .createQueryBuilder('user')
869
- .select(['user.id', 'user.email']) // Select only needed columns
870
- .where('user.isActive = :active', { active: true })
871
- .andWhere('user.createdAt > :date', { date: cutoffDate })
872
- .orderBy('user.createdAt', 'DESC')
873
- .limit(100)
874
- .getMany();
875
-
876
- // Use indexes for better performance
877
- @Index(['email']) // Single column index
878
- @Index(['isActive', 'createdAt']) // Composite index
879
- export class User {
880
- // Entity definition
881
- }
882
- ```
651
+ 1. **Repository Pattern**: Extend TypeOrmRepository for standard PostgreSQL operations
652
+ 2. **Redshift Operations**: Use RedshiftRepository for analytics workloads with specific optimizations
653
+ 3. **Pagination**: Implement TypeOrmPagingRepository for consistent pagination across your app
654
+ 4. **Migrations**: Use migration base classes for consistent schema management
655
+ 5. **SQL Generation**: Use SqlWriter for complex DDL operations
656
+ 6. **Transactions**: Use `forTransaction()` to create transaction-scoped repositories
657
+ 7. **Performance**: For Redshift bulk inserts, use `postOneWithoutReturn()` when you don't need the inserted record back
658
+ 8. **Type Safety**: Leverage the strongly-typed column metadata for compile-time safety
883
659
 
884
660
  ## Testing
885
661
 
886
662
  ```typescript
887
663
  import { Test } from '@nestjs/testing';
888
- import { getRepositoryToken } from '@nestjs/typeorm';
889
- import { Repository } from 'typeorm';
664
+ import { TypeOrmModule } from '@nestjs/typeorm';
665
+ import { EntityManager } from 'typeorm';
666
+ import { UserRepository } from './user.repository';
890
667
  import { User } from './user.entity';
891
- import { UserService } from './user.service';
892
668
 
893
- describe('UserService', () => {
894
- let service: UserService;
895
- let repository: Repository<User>;
669
+ describe('UserRepository', () => {
670
+ let repository: UserRepository;
671
+ let entityManager: EntityManager;
896
672
 
897
673
  beforeEach(async () => {
898
674
  const module = await Test.createTestingModule({
899
- providers: [
900
- UserService,
901
- {
902
- provide: getRepositoryToken(User),
903
- useClass: Repository,
904
- },
675
+ imports: [
676
+ TypeOrmModule.forRoot({
677
+ type: 'postgres',
678
+ host: 'localhost',
679
+ port: 5432,
680
+ username: 'test',
681
+ password: 'test',
682
+ database: 'test_db',
683
+ entities: [User],
684
+ synchronize: true,
685
+ }),
686
+ TypeOrmModule.forFeature([User])
905
687
  ],
688
+ providers: [UserRepository],
906
689
  }).compile();
907
690
 
908
- service = module.get<UserService>(UserService);
909
- repository = module.get<Repository<User>>(getRepositoryToken(User));
691
+ entityManager = module.get<EntityManager>(EntityManager);
692
+ repository = new UserRepository(entityManager);
910
693
  });
911
694
 
912
- it('should perform full-text search', async () => {
913
- const searchResults = await service.searchUsers('john doe');
914
- expect(searchResults.data).toBeDefined();
915
- expect(Array.isArray(searchResults.data)).toBe(true);
695
+ it('should create and retrieve user', async () => {
696
+ const userData = {
697
+ email: 'test@example.com',
698
+ firstName: 'John',
699
+ isActive: true
700
+ };
701
+
702
+ const user = await repository.postOne(userData);
703
+ expect(user.id).toBeDefined();
704
+ expect(user.email).toBe('test@example.com');
705
+
706
+ const foundUser = await repository.getOne({ where: { id: user.id } });
707
+ expect(foundUser).toEqual(user);
708
+ });
709
+
710
+ it('should handle transactions', async () => {
711
+ await entityManager.transaction(async (transactionalEntityManager) => {
712
+ const txRepository = repository.forTransaction(transactionalEntityManager);
713
+
714
+ await txRepository.postOne({
715
+ email: 'tx@example.com',
716
+ firstName: 'Transaction',
717
+ isActive: true
718
+ });
719
+
720
+ // Transaction will be rolled back after test
721
+ });
916
722
  });
917
723
  });
918
724
  ```