@onivoro/server-typeorm-postgres 24.30.12 → 24.30.14

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 (2) hide show
  1. package/README.md +242 -511
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,39 +1,63 @@
1
1
  # @onivoro/server-typeorm-postgres
2
2
 
3
- A TypeORM PostgreSQL integration library providing repository patterns, SQL generation utilities, migration base classes, and PostgreSQL/Redshift-specific optimizations for enterprise-scale applications.
3
+ A TypeORM PostgreSQL integration library providing a NestJS module configuration, enhanced repository patterns, SQL generation utilities, migration base classes, and custom decorators for PostgreSQL and Amazon Redshift applications.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @onivoro/server-typeorm-postgres
8
+ npm install @onivoro/server-typeorm-postgres typeorm-naming-strategies
9
9
  ```
10
10
 
11
- ## Features
11
+ ## Overview
12
12
 
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
19
- - **Type Safety**: Full TypeScript support with comprehensive type definitions
13
+ This library provides:
14
+ - **NestJS Module**: Dynamic module configuration for TypeORM with PostgreSQL
15
+ - **Enhanced Repositories**: `TypeOrmRepository` with additional methods beyond standard TypeORM
16
+ - **Redshift Repository**: Specialized repository for Amazon Redshift with optimizations
17
+ - **SQL Writer**: Static utility for generating PostgreSQL DDL statements
18
+ - **Migration Base Classes**: Simplified classes for common migration operations
19
+ - **Custom Decorators**: Simplified table and column decorators with OpenAPI integration
20
+ - **Pagination Support**: Abstract base class for implementing paginated queries
21
+ - **Utility Functions**: Helper functions for pagination, date queries, and data manipulation
20
22
 
21
- ## Quick Start
22
-
23
- ### Module Import
23
+ ## Module Setup
24
24
 
25
25
  ```typescript
26
26
  import { ServerTypeormPostgresModule } from '@onivoro/server-typeorm-postgres';
27
+ import { User, Product } from './entities';
27
28
 
28
29
  @Module({
29
30
  imports: [
30
- ServerTypeormPostgresModule
31
- ],
31
+ ServerTypeormPostgresModule.configure(
32
+ [UserRepository, ProductRepository], // Injectables
33
+ [User, Product], // Entities
34
+ {
35
+ host: 'localhost',
36
+ port: 5432,
37
+ username: 'postgres',
38
+ password: 'password',
39
+ database: 'myapp',
40
+ ca: process.env.DB_CA, // Optional SSL certificate
41
+ synchronize: false, // Never true in production
42
+ logging: false,
43
+ schema: 'public' // Optional schema
44
+ },
45
+ 'default' // Connection name
46
+ )
47
+ ]
32
48
  })
33
49
  export class AppModule {}
34
50
  ```
35
51
 
36
- ### Entity Definition with Custom Decorators
52
+ The module:
53
+ - Provides `DataSource` and `EntityManager` for injection
54
+ - Caches data sources by name to prevent duplicate connections
55
+ - Uses `SnakeNamingStrategy` for column naming
56
+ - Supports SSL connections with certificate
57
+
58
+ ## Entity Definition with Custom Decorators
59
+
60
+ The library provides simplified decorators that combine TypeORM and OpenAPI functionality:
37
61
 
38
62
  ```typescript
39
63
  import {
@@ -48,34 +72,30 @@ export class User {
48
72
  @PrimaryTableColumn()
49
73
  id: number;
50
74
 
51
- @TableColumn({ type: 'varchar', length: 255, unique: true })
75
+ @TableColumn({ type: 'varchar' })
52
76
  email: string;
53
77
 
54
- @TableColumn({ type: 'varchar', length: 100 })
78
+ @TableColumn({ type: 'varchar' })
55
79
  firstName: string;
56
80
 
57
81
  @NullableTableColumn({ type: 'timestamp' })
58
82
  lastLoginAt?: Date;
59
83
 
60
- @TableColumn({ type: 'boolean', default: true })
84
+ @TableColumn({ type: 'boolean' })
61
85
  isActive: boolean;
62
86
 
63
- @TableColumn({ type: 'jsonb', default: '{}' })
87
+ @TableColumn({ type: 'jsonb' })
64
88
  metadata: Record<string, any>;
65
-
66
- @TableColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
67
- createdAt: Date;
68
-
69
- @NullableTableColumn({ type: 'timestamp' })
70
- deletedAt?: Date;
71
89
  }
72
90
  ```
73
91
 
92
+ **Important**: These decorators only accept the `type` property from TypeORM's `ColumnOptions`. For full control over column options, use TypeORM's decorators directly.
93
+
74
94
  ## Repository Classes
75
95
 
76
96
  ### TypeOrmRepository
77
97
 
78
- Enhanced repository with PostgreSQL-specific methods:
98
+ Enhanced repository with additional convenience methods:
79
99
 
80
100
  ```typescript
81
101
  import { Injectable } from '@nestjs/common';
@@ -89,121 +109,101 @@ export class UserRepository extends TypeOrmRepository<User> {
89
109
  super(User, entityManager);
90
110
  }
91
111
 
92
- // Core methods available:
93
- async findByEmail(email: string): Promise<User> {
94
- return this.getOne({ where: { email } });
95
- }
96
-
97
- async findActiveUsers(): Promise<User[]> {
98
- return this.getMany({
99
- where: { isActive: true }
100
- });
101
- }
102
-
103
- async findUsersWithCount(): Promise<[User[], number]> {
104
- return this.getManyAndCount({
105
- where: { isActive: true }
112
+ // Available methods:
113
+ async findUsers() {
114
+ // getOne - throws if more than one result
115
+ const user = await this.getOne({ where: { id: 1 } });
116
+
117
+ // getMany - returns array
118
+ const activeUsers = await this.getMany({ where: { isActive: true } });
119
+
120
+ // getManyAndCount - returns [items, count]
121
+ const [users, total] = await this.getManyAndCount({
122
+ where: { isActive: true },
123
+ take: 10,
124
+ skip: 0
106
125
  });
107
- }
108
-
109
- async createUser(userData: Partial<User>): Promise<User> {
110
- return this.postOne(userData);
111
- }
112
-
113
- async createUsers(usersData: Partial<User>[]): Promise<User[]> {
114
- return this.postMany(usersData);
115
- }
116
-
117
- async updateUser(id: number, updates: Partial<User>): Promise<void> {
118
- // patch() uses TypeORM's update() method
119
- await this.patch({ id }, updates);
120
- }
121
-
122
- async replaceUser(id: number, userData: Partial<User>): Promise<void> {
123
- // put() uses TypeORM's save() method
124
- await this.put({ id }, userData);
125
- }
126
-
127
- async deleteUser(id: number): Promise<void> {
128
- await this.delete({ id });
129
- }
130
-
131
- async softDeleteUser(id: number): Promise<void> {
132
- await this.softDelete({ id });
126
+
127
+ // postOne - insert and return
128
+ const newUser = await this.postOne({ email: 'test@example.com', firstName: 'Test' });
129
+
130
+ // postMany - bulk insert and return
131
+ const newUsers = await this.postMany([
132
+ { email: 'user1@example.com', firstName: 'User1' },
133
+ { email: 'user2@example.com', firstName: 'User2' }
134
+ ]);
135
+
136
+ // patch - update using TypeORM's update() (doesn't trigger hooks)
137
+ await this.patch({ id: 1 }, { isActive: false });
138
+
139
+ // put - update using TypeORM's save() (triggers hooks)
140
+ await this.put({ id: 1 }, { isActive: false });
141
+
142
+ // delete - hard delete
143
+ await this.delete({ id: 1 });
144
+
145
+ // softDelete - soft delete
146
+ await this.softDelete({ id: 1 });
133
147
  }
134
148
 
135
149
  // Transaction support
136
- async createUserInTransaction(userData: Partial<User>, entityManager: EntityManager): Promise<User> {
137
- const txRepository = this.forTransaction(entityManager);
138
- return txRepository.postOne(userData);
150
+ async updateInTransaction(userId: number, data: Partial<User>, entityManager: EntityManager) {
151
+ const txRepo = this.forTransaction(entityManager);
152
+ await txRepo.patch({ id: userId }, data);
139
153
  }
140
154
 
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]);
155
+ // Raw SQL with mapping
156
+ async customQuery() {
157
+ // query - returns raw results
158
+ const raw = await this.query('SELECT * FROM users WHERE created_at > $1', [new Date('2024-01-01')]);
159
+
160
+ // queryAndMap - maps results to entity type
161
+ const users = await this.queryAndMap('SELECT * FROM users WHERE active = $1', [true]);
149
162
  }
150
163
 
151
- // Using ILike for case-insensitive search
152
- async searchUsers(searchTerm: string): Promise<User[]> {
164
+ // ILike helper for case-insensitive search
165
+ async searchUsers(term: string) {
153
166
  const filters = this.buildWhereILike({
154
- firstName: searchTerm,
155
- lastName: searchTerm,
156
- email: searchTerm
167
+ firstName: term,
168
+ email: term
157
169
  });
158
-
159
170
  return this.getMany({ where: filters });
160
171
  }
161
172
  }
162
173
  ```
163
174
 
175
+ The repository also provides access to:
176
+ - `repo` - The underlying TypeORM repository
177
+ - `columns` - Metadata about entity columns
178
+ - `table` - Table name
179
+ - `schema` - Schema name
180
+
164
181
  ### TypeOrmPagingRepository
165
182
 
166
- Abstract base class for implementing pagination:
183
+ Abstract base class requiring implementation of `getPage`:
167
184
 
168
185
  ```typescript
169
186
  import { Injectable } from '@nestjs/common';
170
- import { TypeOrmPagingRepository, IPageParams, IPagedData } from '@onivoro/server-typeorm-postgres';
171
- import { EntityManager, FindManyOptions } from 'typeorm';
187
+ import { TypeOrmPagingRepository, IPageParams, IPagedData, getSkip, getPagingKey, removeFalseyKeys } from '@onivoro/server-typeorm-postgres';
188
+ import { EntityManager } from 'typeorm';
172
189
  import { User } from './user.entity';
173
190
 
174
- // Define your custom params interface
175
- interface UserPageParams {
176
- isActive?: boolean;
177
- search?: string;
178
- departmentId?: number;
179
- }
180
-
181
191
  @Injectable()
182
- export class UserPagingRepository extends TypeOrmPagingRepository<User, UserPageParams> {
192
+ export class UserPagingRepository extends TypeOrmPagingRepository<User, UserSearchParams> {
183
193
  constructor(entityManager: EntityManager) {
184
194
  super(User, entityManager);
185
195
  }
186
196
 
187
- // You must implement the abstract getPage method
188
- async getPage(pageParams: IPageParams, params: UserPageParams): Promise<IPagedData<User>> {
197
+ // Must implement this abstract method
198
+ async getPage(pageParams: IPageParams, params: UserSearchParams): Promise<IPagedData<User>> {
189
199
  const { page, limit } = pageParams;
190
- const skip = this.getSkip(page, limit);
200
+ const skip = getSkip(page, limit);
191
201
 
192
- // Build where conditions
193
- const where = this.removeFalseyKeys({
194
- isActive: params.isActive,
195
- departmentId: params.departmentId
202
+ const where = removeFalseyKeys({
203
+ departmentId: params.departmentId,
204
+ isActive: params.isActive
196
205
  });
197
206
 
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
- }
206
-
207
207
  const [data, total] = await this.getManyAndCount({
208
208
  where,
209
209
  skip,
@@ -221,17 +221,12 @@ export class UserPagingRepository extends TypeOrmPagingRepository<User, UserPage
221
221
  hasPrev: page > 1
222
222
  };
223
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
- }
229
224
  }
230
225
  ```
231
226
 
232
- ### RedshiftRepository
227
+ ### RedshiftRepository
233
228
 
234
- Specialized repository for Amazon Redshift with custom SQL building:
229
+ Specialized repository for Amazon Redshift with different SQL generation:
235
230
 
236
231
  ```typescript
237
232
  import { Injectable } from '@nestjs/common';
@@ -244,485 +239,221 @@ export class AnalyticsRepository extends RedshiftRepository<AnalyticsEvent> {
244
239
  constructor(entityManager: EntityManager) {
245
240
  super(AnalyticsEvent, entityManager);
246
241
  }
247
-
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);
252
- }
253
-
254
- async bulkInsertEvents(events: Partial<AnalyticsEvent>[]): Promise<AnalyticsEvent[]> {
255
- // Uses optimized bulk insert
256
- return this.postMany(events);
257
- }
258
-
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 = `
273
- SELECT
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
- `;
242
+
243
+ // Optimized methods for Redshift
244
+ async bulkInsertEvents(events: Partial<AnalyticsEvent>[]) {
245
+ // postOne - performs manual insert + select
246
+ const event = await this.postOne({ type: 'click', userId: 123 });
283
247
 
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
- `;
248
+ // postOneWithoutReturn - insert only, no select (better performance)
249
+ await this.postOneWithoutReturn({ type: 'view', userId: 456 });
250
+
251
+ // postMany - bulk insert with retrieval
252
+ const inserted = await this.postMany(events);
294
253
 
295
- return this.queryAndMap(query, [value]);
254
+ // postManyWithoutReturn - NOT IMPLEMENTED (throws error)
255
+ // await this.postManyWithoutReturn(events);
296
256
  }
297
257
  }
298
258
  ```
299
259
 
260
+ Key differences in RedshiftRepository:
261
+ - Uses raw SQL instead of TypeORM query builder for better Redshift compatibility
262
+ - Automatically wraps JSONB values with `JSON_PARSE()`
263
+ - `getMany`, `getOne`, `delete`, `patch` use custom SQL generation
264
+ - `put`, `forTransaction`, `getManyAndCount` throw `NotImplementedException`
265
+ - `softDelete` uses patch with `deletedAt` field
266
+
300
267
  ## SQL Writer
301
268
 
302
269
  Static utility class for generating PostgreSQL DDL:
303
270
 
304
271
  ```typescript
305
272
  import { SqlWriter } from '@onivoro/server-typeorm-postgres';
306
- import { TableColumnOptions } from 'typeorm';
307
273
 
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)
274
+ // All methods return SQL strings
275
+ const sql1 = SqlWriter.addColumn('users', { name: 'phone', type: 'varchar', length: 20 });
276
+ // Returns: ALTER TABLE "users" ADD "phone" varchar(20)
316
277
 
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' }
278
+ const sql2 = SqlWriter.addColumns('users', [
279
+ { name: 'phone', type: 'varchar', length: 20 },
280
+ { name: 'address', type: 'jsonb', nullable: true }
324
281
  ]);
325
282
 
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 }
283
+ const sql3 = SqlWriter.createTable('products', [
284
+ { name: 'id', type: 'serial', isPrimary: true },
285
+ { name: 'name', type: 'varchar', length: 255 },
286
+ { name: 'price', type: 'decimal', precision: 10, scale: 2 }
334
287
  ]);
335
288
 
336
- // Drop column
337
- const dropColumnSql = SqlWriter.dropColumn('products', { name: 'old_column' });
338
- // Returns: ALTER TABLE "products" DROP COLUMN old_column
289
+ const sql4 = SqlWriter.dropTable('old_table');
290
+ // Returns: DROP TABLE "old_table";
339
291
 
340
- // Create indexes
341
- const createIndexSql = SqlWriter.createIndex('products', 'name', false);
342
- // Returns: CREATE INDEX IF NOT EXISTS products_name ON "products"(name)
292
+ const sql5 = SqlWriter.dropColumn('users', { name: 'old_column' });
293
+ // Returns: ALTER TABLE "users" DROP COLUMN old_column
343
294
 
344
- const createUniqueIndexSql = SqlWriter.createUniqueIndex('products', 'sku');
345
- // Returns: CREATE UNIQUE INDEX IF NOT EXISTS products_sku ON "products"(sku)
295
+ const sql6 = SqlWriter.createIndex('users', 'email', false);
296
+ // Returns: CREATE INDEX IF NOT EXISTS users_email ON "users"(email)
346
297
 
347
- // Drop index
348
- const dropIndexSql = SqlWriter.dropIndex('products_name');
349
- // Returns: DROP INDEX IF EXISTS products_name
298
+ const sql7 = SqlWriter.createUniqueIndex('users', 'username');
299
+ // Returns: CREATE UNIQUE INDEX IF NOT EXISTS users_username ON "users"(username)
350
300
 
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
301
+ const sql8 = SqlWriter.dropIndex('users_email');
302
+ // Returns: DROP INDEX IF EXISTS users_email
367
303
  ```
368
304
 
305
+ Special handling for defaults:
306
+ - JSONB values are stringified and cast: `'{"key":"value"}'::jsonb`
307
+ - Booleans become `TRUE`/`FALSE`
308
+ - Other values are quoted appropriately
309
+
369
310
  ## Migration Base Classes
370
311
 
371
- ### TableMigrationBase
312
+ Simplified migration classes that implement TypeORM's up/down methods:
372
313
 
373
314
  ```typescript
374
- import { TableMigrationBase } from '@onivoro/server-typeorm-postgres';
375
- import { MigrationInterface } from 'typeorm';
315
+ import {
316
+ TableMigrationBase,
317
+ ColumnMigrationBase,
318
+ ColumnsMigrationBase,
319
+ IndexMigrationBase,
320
+ DropTableMigrationBase,
321
+ DropColumnMigrationBase
322
+ } from '@onivoro/server-typeorm-postgres';
376
323
 
377
- export class CreateUsersTable1234567890 extends TableMigrationBase implements MigrationInterface {
324
+ // Create table
325
+ export class CreateUsersTable1234567890 extends TableMigrationBase {
378
326
  constructor() {
379
327
  super('users', [
380
328
  { 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 }
329
+ { name: 'email', type: 'varchar', length: 255, isUnique: true }
387
330
  ]);
388
331
  }
389
332
  }
390
- ```
391
-
392
- ### ColumnMigrationBase
393
-
394
- ```typescript
395
- import { ColumnMigrationBase } from '@onivoro/server-typeorm-postgres';
396
333
 
397
- export class AddUserPhoneNumber1234567891 extends ColumnMigrationBase {
334
+ // Add single column
335
+ export class AddUserPhone1234567891 extends ColumnMigrationBase {
398
336
  constructor() {
399
- super('users', {
400
- name: 'phone_number',
401
- type: 'varchar',
402
- length: 20,
403
- isNullable: true
404
- });
337
+ super('users', { name: 'phone', type: 'varchar', length: 20, isNullable: true });
405
338
  }
406
339
  }
407
- ```
408
340
 
409
- ### ColumnsMigrationBase
410
-
411
- ```typescript
412
- import { ColumnsMigrationBase } from '@onivoro/server-typeorm-postgres';
413
-
414
- export class AddUserContactInfo1234567892 extends ColumnsMigrationBase {
341
+ // Add multiple columns
342
+ export class AddUserDetails1234567892 extends ColumnsMigrationBase {
415
343
  constructor() {
416
344
  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 }
345
+ { name: 'phone', type: 'varchar', length: 20 },
346
+ { name: 'address', type: 'jsonb' }
420
347
  ]);
421
348
  }
422
349
  }
423
- ```
424
-
425
- ### IndexMigrationBase
426
350
 
427
- ```typescript
428
- import { IndexMigrationBase } from '@onivoro/server-typeorm-postgres';
429
-
430
- export class CreateUserEmailIndex1234567893 extends IndexMigrationBase {
351
+ // Create index
352
+ export class IndexUserEmail1234567893 extends IndexMigrationBase {
431
353
  constructor() {
432
- super('users', 'email', true); // table, column, unique
354
+ super('users', 'email', true); // table, column, isUnique
433
355
  }
434
356
  }
435
- ```
436
357
 
437
- ### DropTableMigrationBase & DropColumnMigrationBase
438
-
439
- ```typescript
440
- import { DropTableMigrationBase, DropColumnMigrationBase } from '@onivoro/server-typeorm-postgres';
441
-
442
- export class DropLegacyUsersTable1234567894 extends DropTableMigrationBase {
358
+ // Drop table
359
+ export class DropOldTable1234567894 extends DropTableMigrationBase {
443
360
  constructor() {
444
361
  super('legacy_users');
445
362
  }
446
363
  }
447
364
 
448
- export class DropUserMiddleName1234567895 extends DropColumnMigrationBase {
365
+ // Drop column
366
+ export class DropMiddleName1234567895 extends DropColumnMigrationBase {
449
367
  constructor() {
450
368
  super('users', { name: 'middle_name' });
451
369
  }
452
370
  }
453
371
  ```
454
372
 
455
- ## Building Repositories from Metadata
456
-
457
- Both TypeOrmRepository and RedshiftRepository support building instances from metadata:
458
-
459
- ```typescript
460
- import { TypeOrmRepository, RedshiftRepository } from '@onivoro/server-typeorm-postgres';
461
- import { DataSource } from 'typeorm';
462
-
463
- // Define your entity type
464
- interface UserEvent {
465
- id: number;
466
- userId: number;
467
- eventType: string;
468
- eventData: any;
469
- createdAt: Date;
470
- }
471
-
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'
511
- }
512
- }
513
- });
514
-
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
521
- }
522
- });
523
-
524
- // Use the repositories
525
- const events = await userEventRepo.getMany({ where: { userId: 123 } });
526
- const recentEvent = await userEventRepo.getOne({ where: { id: 456 } });
527
- ```
528
-
529
- ## Data Source Configuration
530
-
531
- ```typescript
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]);
553
-
554
- const dataSource = new DataSource(config);
555
- ```
556
-
557
- ## Type Definitions
558
-
559
- ### Core Types
560
-
561
- ```typescript
562
- // Table metadata
563
- interface TTableMeta {
564
- databasePath: string;
565
- type: string;
566
- propertyPath: string;
567
- isPrimary: boolean;
568
- default?: any;
569
- }
570
-
571
- // Page parameters
572
- interface IPageParams {
573
- page: number;
574
- limit: number;
575
- }
576
-
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
- }
587
-
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
- }
598
-
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>;
610
- }
611
- ```
612
-
613
373
  ## Utility Functions
614
374
 
615
375
  ```typescript
616
376
  import {
617
- getSkip,
618
- getPagingKey,
377
+ getSkip,
378
+ getPagingKey,
619
379
  removeFalseyKeys,
620
380
  generateDateQuery,
621
- getApiTypeFromColumn
381
+ getApiTypeFromColumn,
382
+ dataSourceFactory,
383
+ dataSourceConfigFactory
622
384
  } from '@onivoro/server-typeorm-postgres';
623
385
 
624
- // Calculate skip value for pagination
386
+ // Pagination helpers
625
387
  const skip = getSkip(2, 20); // page 2, limit 20 = skip 20
388
+ const cacheKey = getPagingKey(2, 20); // "page_2_limit_20"
626
389
 
627
- // Generate cache key for pagination
628
- const cacheKey = getPagingKey(2, 20); // Returns: "page_2_limit_20"
629
-
630
- // Remove falsey values from object
631
- const cleanedFilters = removeFalseyKeys({
390
+ // Remove null/undefined/empty string values
391
+ const clean = removeFalseyKeys({
632
392
  name: 'John',
633
- age: 0, // Removed
634
- active: false, // Kept (false is not falsey for this function)
635
- email: '', // Removed
636
- dept: null // Removed
393
+ age: null, // removed
394
+ email: '', // removed
395
+ active: false // kept
637
396
  });
638
397
 
639
- // Generate date range query
640
- const dateQuery = generateDateQuery('created_at', {
398
+ // Date range query builder
399
+ const dateFilter = generateDateQuery('created_at', {
641
400
  startDate: new Date('2024-01-01'),
642
401
  endDate: new Date('2024-12-31')
643
402
  });
403
+ // Returns TypeORM Between operator
644
404
 
645
- // Get API type from TypeORM column metadata
646
- const apiType = getApiTypeFromColumn(columnMetadata);
647
- ```
405
+ // Column type to API type mapping
406
+ const apiType = getApiTypeFromColumn('varchar'); // 'string'
407
+ const apiType2 = getApiTypeFromColumn('int'); // 'number'
408
+ const apiType3 = getApiTypeFromColumn('jsonb'); // 'object'
648
409
 
649
- ## Best Practices
410
+ // Create data source
411
+ const ds = dataSourceFactory('main', {
412
+ host: 'localhost',
413
+ port: 5432,
414
+ username: 'user',
415
+ password: 'pass',
416
+ database: 'db'
417
+ }, [User, Product]);
418
+ ```
650
419
 
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
420
+ ## Building Repositories from Metadata
659
421
 
660
- ## Testing
422
+ For dynamic entity handling without TypeORM decorators:
661
423
 
662
424
  ```typescript
663
- import { Test } from '@nestjs/testing';
664
- import { TypeOrmModule } from '@nestjs/typeorm';
665
- import { EntityManager } from 'typeorm';
666
- import { UserRepository } from './user.repository';
667
- import { User } from './user.entity';
425
+ const metadata = {
426
+ schema: 'public',
427
+ table: 'events',
428
+ columns: {
429
+ id: { databasePath: 'id', type: 'int', propertyPath: 'id', isPrimary: true, default: undefined },
430
+ type: { databasePath: 'event_type', type: 'varchar', propertyPath: 'type', isPrimary: false, default: undefined },
431
+ data: { databasePath: 'event_data', type: 'jsonb', propertyPath: 'data', isPrimary: false, default: {} }
432
+ }
433
+ };
668
434
 
669
- describe('UserRepository', () => {
670
- let repository: UserRepository;
671
- let entityManager: EntityManager;
672
-
673
- beforeEach(async () => {
674
- const module = await Test.createTestingModule({
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])
687
- ],
688
- providers: [UserRepository],
689
- }).compile();
690
-
691
- entityManager = module.get<EntityManager>(EntityManager);
692
- repository = new UserRepository(entityManager);
693
- });
694
-
695
- it('should create and retrieve user', async () => {
696
- const userData = {
697
- email: 'test@example.com',
698
- firstName: 'John',
699
- isActive: true
700
- };
435
+ // Build TypeORM repository
436
+ const eventRepo = TypeOrmRepository.buildFromMetadata(dataSource, metadata);
701
437
 
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
- });
722
- });
723
- });
438
+ // Build Redshift repository
439
+ const analyticsRepo = RedshiftRepository.buildFromMetadata(redshiftDataSource, metadata);
724
440
  ```
725
441
 
442
+ ## Important Implementation Details
443
+
444
+ 1. **Data Source Caching**: The module caches data sources by name to prevent multiple connections
445
+ 2. **Snake Case**: All database columns use snake_case via `SnakeNamingStrategy`
446
+ 3. **Column Decorators**: The custom decorators are thin wrappers - use TypeORM decorators for full control
447
+ 4. **Repository Methods**:
448
+ - `patch` uses TypeORM's `update()` - doesn't trigger entity hooks
449
+ - `put` uses TypeORM's `save()` - triggers entity hooks
450
+ - `getOne` throws error if multiple results found
451
+ 5. **Redshift Limitations**:
452
+ - No transaction support
453
+ - No `getManyAndCount`
454
+ - `postManyWithoutReturn` not implemented
455
+ - Automatic `JSON_PARSE()` wrapping for JSONB columns
456
+
726
457
  ## License
727
458
 
728
- This library is licensed under the MIT License. See the LICENSE file in this package for details.
459
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onivoro/server-typeorm-postgres",
3
- "version": "24.30.12",
3
+ "version": "24.30.14",
4
4
  "license": "MIT",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",