@onivoro/server-typeorm-postgres 22.0.12 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +922 -0
  2. package/{dist/esm/index.d.ts → index.ts} +6 -1
  3. package/jest.config.ts +11 -0
  4. package/package.json +8 -47
  5. package/project.json +23 -0
  6. package/src/index.ts +33 -0
  7. package/src/lib/classes/__snapshots__/sql-writer.class.spec.ts.snap +8 -0
  8. package/src/lib/classes/column-migration-base.class.ts +17 -0
  9. package/src/lib/classes/columns-migration-base.class.ts +17 -0
  10. package/src/lib/classes/drop-column-migration-base.class.ts +17 -0
  11. package/src/lib/classes/drop-table-migration-base.class.ts +17 -0
  12. package/src/lib/classes/index-migration-base.class.ts +17 -0
  13. package/src/lib/classes/redshift-repository.class.ts +146 -0
  14. package/src/lib/classes/sql-writer.class.spec.ts +20 -0
  15. package/{dist/esm/lib/classes/sql-writer.class.js → src/lib/classes/sql-writer.class.ts} +29 -16
  16. package/src/lib/classes/table-migration-base.class.ts +17 -0
  17. package/src/lib/classes/type-orm-paging-repository.class.ts +22 -0
  18. package/src/lib/classes/type-orm-repository.class.ts +299 -0
  19. package/src/lib/constants/many-to-one-relation-options.constant.ts +3 -0
  20. package/src/lib/decorators/nullable-table-column.decorator.ts +9 -0
  21. package/src/lib/decorators/primary-table-column.decorator.ts +9 -0
  22. package/src/lib/decorators/table-column.decorator.ts +9 -0
  23. package/src/lib/decorators/table.decorator.ts +8 -0
  24. package/src/lib/functions/data-source-config-factory.function.ts +34 -0
  25. package/src/lib/functions/data-source-factory.function.ts +10 -0
  26. package/src/lib/functions/generate-date-query.function.ts +20 -0
  27. package/src/lib/functions/get-api-type-from-column.function.ts +14 -0
  28. package/src/lib/functions/get-paging-key.function.ts +3 -0
  29. package/src/lib/functions/get-skip.function.ts +3 -0
  30. package/src/lib/functions/remove-falsey-keys.function.ts +8 -0
  31. package/src/lib/server-typeorm-postgres.module.ts +44 -0
  32. package/src/lib/types/data-source-options.interface.ts +11 -0
  33. package/{dist/esm/lib/types/entity-provider.interface.d.ts → src/lib/types/entity-provider.interface.ts} +2 -2
  34. package/{dist/esm/lib/types/page-params.interface.d.ts → src/lib/types/page-params.interface.ts} +2 -0
  35. package/src/lib/types/table-meta.type.ts +4 -0
  36. package/tsconfig.json +17 -0
  37. package/tsconfig.lib.json +8 -0
  38. package/tsconfig.spec.json +21 -0
  39. package/dist/esm/index.js +0 -44
  40. package/dist/esm/lib/classes/column-migration-base.class.d.ts +0 -8
  41. package/dist/esm/lib/classes/column-migration-base.class.js +0 -19
  42. package/dist/esm/lib/classes/columns-migration-base.class.d.ts +0 -8
  43. package/dist/esm/lib/classes/columns-migration-base.class.js +0 -19
  44. package/dist/esm/lib/classes/drop-column-migration-base.class.d.ts +0 -8
  45. package/dist/esm/lib/classes/drop-column-migration-base.class.js +0 -19
  46. package/dist/esm/lib/classes/drop-table-migration-base.class.d.ts +0 -8
  47. package/dist/esm/lib/classes/drop-table-migration-base.class.js +0 -19
  48. package/dist/esm/lib/classes/index-migration-base.class.d.ts +0 -8
  49. package/dist/esm/lib/classes/index-migration-base.class.js +0 -19
  50. package/dist/esm/lib/classes/redshift-repository.class.d.ts +0 -29
  51. package/dist/esm/lib/classes/redshift-repository.class.js +0 -129
  52. package/dist/esm/lib/classes/sql-writer.class.d.ts +0 -14
  53. package/dist/esm/lib/classes/table-migration-base.class.d.ts +0 -8
  54. package/dist/esm/lib/classes/table-migration-base.class.js +0 -19
  55. package/dist/esm/lib/classes/type-orm-paging-repository.class.d.ts +0 -14
  56. package/dist/esm/lib/classes/type-orm-paging-repository.class.js +0 -16
  57. package/dist/esm/lib/classes/type-orm-repository.class.d.ts +0 -62
  58. package/dist/esm/lib/classes/type-orm-repository.class.js +0 -202
  59. package/dist/esm/lib/constants/many-to-one-relation-options.constant.d.ts +0 -2
  60. package/dist/esm/lib/constants/many-to-one-relation-options.constant.js +0 -4
  61. package/dist/esm/lib/decorators/nullable-table-column.decorator.d.ts +0 -2
  62. package/dist/esm/lib/decorators/nullable-table-column.decorator.js +0 -12
  63. package/dist/esm/lib/decorators/primary-table-column.decorator.d.ts +0 -2
  64. package/dist/esm/lib/decorators/primary-table-column.decorator.js +0 -12
  65. package/dist/esm/lib/decorators/table-column.decorator.d.ts +0 -2
  66. package/dist/esm/lib/decorators/table-column.decorator.js +0 -12
  67. package/dist/esm/lib/decorators/table.decorator.d.ts +0 -3
  68. package/dist/esm/lib/decorators/table.decorator.js +0 -11
  69. package/dist/esm/lib/functions/data-source-config-factory.function.d.ts +0 -3
  70. package/dist/esm/lib/functions/data-source-config-factory.function.js +0 -25
  71. package/dist/esm/lib/functions/data-source-factory.function.d.ts +0 -3
  72. package/dist/esm/lib/functions/data-source-factory.function.js +0 -7
  73. package/dist/esm/lib/functions/generate-date-query.function.d.ts +0 -2
  74. package/dist/esm/lib/functions/generate-date-query.function.js +0 -16
  75. package/dist/esm/lib/functions/get-api-type-from-column.function.d.ts +0 -2
  76. package/dist/esm/lib/functions/get-api-type-from-column.function.js +0 -14
  77. package/dist/esm/lib/functions/get-paging-key.function.d.ts +0 -1
  78. package/dist/esm/lib/functions/get-paging-key.function.js +0 -6
  79. package/dist/esm/lib/functions/get-skip.function.d.ts +0 -1
  80. package/dist/esm/lib/functions/get-skip.function.js +0 -6
  81. package/dist/esm/lib/functions/remove-falsey-keys.function.d.ts +0 -1
  82. package/dist/esm/lib/functions/remove-falsey-keys.function.js +0 -11
  83. package/dist/esm/lib/server-typeorm-postgres.module.d.ts +0 -5
  84. package/dist/esm/lib/server-typeorm-postgres.module.js +0 -49
  85. package/dist/esm/lib/types/data-source-options.interface.d.ts +0 -11
  86. package/dist/esm/lib/types/data-source-options.interface.js +0 -2
  87. package/dist/esm/lib/types/entity-provider.interface.js +0 -2
  88. package/dist/esm/lib/types/page-params.interface.js +0 -2
  89. package/dist/esm/lib/types/paged-data.interface.js +0 -2
  90. package/dist/esm/lib/types/table-meta.type.d.ts +0 -9
  91. package/dist/esm/lib/types/table-meta.type.js +0 -2
  92. /package/{dist/esm/lib/types/paged-data.interface.d.ts → src/lib/types/paged-data.interface.ts} +0 -0
package/README.md ADDED
@@ -0,0 +1,922 @@
1
+ # @onivoro/server-typeorm-postgres
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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @onivoro/server-typeorm-postgres
9
+ ```
10
+
11
+ ## Features
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
21
+ - **Type Safety**: Full TypeScript support with comprehensive type definitions
22
+ - **PostgreSQL Optimizations**: PostgreSQL-specific optimizations and best practices
23
+
24
+ ## Quick Start
25
+
26
+ ### Import the Module
27
+
28
+ ```typescript
29
+ import { ServerTypeormPostgresModule } from '@onivoro/server-typeorm-postgres';
30
+
31
+ @Module({
32
+ 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
+ })
44
+ ],
45
+ })
46
+ export class AppModule {}
47
+ ```
48
+
49
+ ### Define Entities with Custom Decorators
50
+
51
+ ```typescript
52
+ import {
53
+ Table,
54
+ PrimaryTableColumn,
55
+ TableColumn,
56
+ NullableTableColumn
57
+ } from '@onivoro/server-typeorm-postgres';
58
+ import { Entity } from 'typeorm';
59
+
60
+ @Entity()
61
+ @Table('users')
62
+ export class User {
63
+ @PrimaryTableColumn()
64
+ id: number;
65
+
66
+ @TableColumn({ type: 'varchar', length: 255, unique: true })
67
+ email: string;
68
+
69
+ @TableColumn({ type: 'varchar', length: 100 })
70
+ firstName: string;
71
+
72
+ @TableColumn({ type: 'varchar', length: 100 })
73
+ lastName: string;
74
+
75
+ @NullableTableColumn({ type: 'timestamp' })
76
+ lastLoginAt?: Date;
77
+
78
+ @TableColumn({ type: 'boolean', default: true })
79
+ isActive: boolean;
80
+
81
+ @TableColumn({ type: 'jsonb' })
82
+ metadata: Record<string, any>;
83
+
84
+ @TableColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
85
+ createdAt: Date;
86
+
87
+ @TableColumn({
88
+ type: 'timestamp',
89
+ default: () => 'CURRENT_TIMESTAMP',
90
+ onUpdate: 'CURRENT_TIMESTAMP'
91
+ })
92
+ updatedAt: Date;
93
+
94
+ @NullableTableColumn({ type: 'timestamp' })
95
+ deletedAt?: Date;
96
+ }
97
+ ```
98
+
99
+ ### Use Custom Repository
100
+
101
+ ```typescript
102
+ import { Injectable } from '@nestjs/common';
103
+ import { TypeOrmRepository, TypeOrmPagingRepository } from '@onivoro/server-typeorm-postgres';
104
+ import { User } from './user.entity';
105
+
106
+ @Injectable()
107
+ export class UserRepository extends TypeOrmPagingRepository<User> {
108
+ constructor() {
109
+ super(User);
110
+ }
111
+
112
+ async findByEmail(email: string): Promise<User | null> {
113
+ return this.findOne({ where: { email } });
114
+ }
115
+
116
+ async findActiveUsers(): Promise<User[]> {
117
+ return this.find({
118
+ where: { isActive: true, deletedAt: null }
119
+ });
120
+ }
121
+
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();
128
+ }
129
+
130
+ async softDelete(id: number): Promise<void> {
131
+ await this.update(id, { deletedAt: new Date() });
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Configuration
137
+
138
+ ### Data Source Configuration
139
+
140
+ ```typescript
141
+ import { dataSourceConfigFactory } from '@onivoro/server-typeorm-postgres';
142
+
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,
158
+ }
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
+
193
+ ## Usage Examples
194
+
195
+ ### Migration Base Classes
196
+
197
+ ```typescript
198
+ import {
199
+ TableMigrationBase,
200
+ ColumnMigrationBase,
201
+ IndexMigrationBase,
202
+ DropTableMigrationBase
203
+ } from '@onivoro/server-typeorm-postgres';
204
+ import { MigrationInterface, QueryRunner } from 'typeorm';
205
+
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
+ ]);
219
+
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']);
224
+ }
225
+
226
+ public async down(queryRunner: QueryRunner): Promise<void> {
227
+ await this.dropTable(queryRunner, 'users');
228
+ }
229
+ }
230
+
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 });
236
+ }
237
+
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');
242
+ }
243
+ }
244
+ ```
245
+
246
+ ### Advanced Repository Usage
247
+
248
+ ```typescript
249
+ import { Injectable } from '@nestjs/common';
250
+ import { TypeOrmPagingRepository, PageParams, PagedData } from '@onivoro/server-typeorm-postgres';
251
+ import { User } from './user.entity';
252
+ import { FindOptionsWhere, ILike, Raw, Between } from 'typeorm';
253
+
254
+ @Injectable()
255
+ export class AdvancedUserRepository extends TypeOrmPagingRepository<User> {
256
+ constructor() {
257
+ super(User);
258
+ }
259
+
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
+ );
272
+ }
273
+
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
+ }
289
+
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
+ }
301
+
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
+ }
318
+
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();
337
+
338
+ 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)
344
+ };
345
+ }
346
+ }
347
+ ```
348
+
349
+ ### Redshift Integration
350
+
351
+ ```typescript
352
+ import { Injectable } from '@nestjs/common';
353
+ import { RedshiftRepository } from '@onivoro/server-typeorm-postgres';
354
+
355
+ @Injectable()
356
+ export class AnalyticsRepository extends RedshiftRepository {
357
+ constructor() {
358
+ super();
359
+ }
360
+
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]);
376
+ }
377
+
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
+ `);
389
+ }
390
+
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
+ )
404
+ 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
+ `);
414
+ }
415
+ }
416
+ ```
417
+
418
+ ### SQL Writer Utilities
419
+
420
+ ```typescript
421
+ import { Injectable } from '@nestjs/common';
422
+ 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
+ }
457
+
458
+ if (filters.endDate) {
459
+ query.andWhere('u.created_at <= :endDate', { endDate: filters.endDate });
460
+ }
461
+
462
+ if (filters.segment) {
463
+ query.andWhere("u.metadata->>'segment' = :segment", { segment: filters.segment });
464
+ }
465
+
466
+ if (filters.isActive !== undefined) {
467
+ query.andWhere('u.is_active = :isActive', { isActive: filters.isActive });
468
+ }
469
+
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();
474
+ }
475
+
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
+ };
504
+
505
+ const results = await Promise.all(
506
+ Object.entries(queries).map(async ([key, query]) => [
507
+ key,
508
+ await query.getRawOne()
509
+ ])
510
+ );
511
+
512
+ return Object.fromEntries(results);
513
+ }
514
+ }
515
+ ```
516
+
517
+ ### Complex Entity Relationships
518
+
519
+ ```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;
536
+
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[];
578
+ }
579
+
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;
592
+
593
+ @TableColumn({ type: 'int' })
594
+ quantity: number;
595
+
596
+ @TableColumn({ type: 'decimal', precision: 10, scale: 2 })
597
+ unitPrice: number;
598
+
599
+ @TableColumn({ type: 'decimal', precision: 10, scale: 2 })
600
+ totalPrice: number;
601
+
602
+ @TableColumn({ type: 'jsonb', default: '{}' })
603
+ productSnapshot: Record<string, any>;
604
+
605
+ @ManyToOne(() => Order, order => order.items, ManyToOneRelationOptions)
606
+ @JoinColumn({ name: 'orderId' })
607
+ order: Order;
608
+
609
+ @ManyToOne(() => Product, product => product.orderItems, ManyToOneRelationOptions)
610
+ @JoinColumn({ name: 'productId' })
611
+ product: Product;
612
+ }
613
+ ```
614
+
615
+ ### Advanced PostgreSQL Features
616
+
617
+ ```typescript
618
+ import { Injectable } from '@nestjs/common';
619
+ import { DataSource } from 'typeorm';
620
+
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
+ }
695
+
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
+ `);
717
+ }
718
+ }
719
+
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]);
740
+ }
741
+
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
+ }
751
+ ```
752
+
753
+ ## API Reference
754
+
755
+ ### Repository Classes
756
+
757
+ #### TypeOrmRepository<T>
758
+
759
+ Base repository class with PostgreSQL optimizations:
760
+
761
+ ```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:
770
+
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
+ }
778
+ ```
779
+
780
+ #### RedshiftRepository
781
+
782
+ Repository for Amazon Redshift operations:
783
+
784
+ ```typescript
785
+ export class RedshiftRepository {
786
+ async query(sql: string, parameters?: any[]): Promise<any[]>
787
+ async execute(sql: string, parameters?: any[]): Promise<void>
788
+ }
789
+ ```
790
+
791
+ ### Migration Base Classes
792
+
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>
802
+ }
803
+ ```
804
+
805
+ #### ColumnMigrationBase
806
+
807
+ Base class for column modifications:
808
+
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>
814
+ }
815
+ ```
816
+
817
+ ### SQL Writer
818
+
819
+ #### SqlWriter
820
+
821
+ Advanced SQL query builder:
822
+
823
+ ```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
+ ```
836
+
837
+ ### Type Definitions
838
+
839
+ #### TableMeta
840
+
841
+ Table metadata type:
842
+
843
+ ```typescript
844
+ interface TableMeta {
845
+ name: string;
846
+ schema?: string;
847
+ columns: ColumnMeta[];
848
+ indexes: IndexMeta[];
849
+ }
850
+ ```
851
+
852
+ ## Best Practices
853
+
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
+ ```
883
+
884
+ ## Testing
885
+
886
+ ```typescript
887
+ import { Test } from '@nestjs/testing';
888
+ import { getRepositoryToken } from '@nestjs/typeorm';
889
+ import { Repository } from 'typeorm';
890
+ import { User } from './user.entity';
891
+ import { UserService } from './user.service';
892
+
893
+ describe('UserService', () => {
894
+ let service: UserService;
895
+ let repository: Repository<User>;
896
+
897
+ beforeEach(async () => {
898
+ const module = await Test.createTestingModule({
899
+ providers: [
900
+ UserService,
901
+ {
902
+ provide: getRepositoryToken(User),
903
+ useClass: Repository,
904
+ },
905
+ ],
906
+ }).compile();
907
+
908
+ service = module.get<UserService>(UserService);
909
+ repository = module.get<Repository<User>>(getRepositoryToken(User));
910
+ });
911
+
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);
916
+ });
917
+ });
918
+ ```
919
+
920
+ ## License
921
+
922
+ This library is part of the Onivoro monorepo ecosystem.