@onivoro/server-typeorm-postgres 22.0.13 → 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.
- package/README.md +922 -0
- package/{dist/cjs/index.d.ts → index.ts} +6 -1
- package/jest.config.ts +11 -0
- package/package.json +8 -47
- package/project.json +23 -0
- package/{dist/esm/index.d.ts → src/index.ts} +6 -1
- package/src/lib/classes/__snapshots__/sql-writer.class.spec.ts.snap +8 -0
- package/src/lib/classes/column-migration-base.class.ts +17 -0
- package/src/lib/classes/columns-migration-base.class.ts +17 -0
- package/src/lib/classes/drop-column-migration-base.class.ts +17 -0
- package/src/lib/classes/drop-table-migration-base.class.ts +17 -0
- package/src/lib/classes/index-migration-base.class.ts +17 -0
- package/src/lib/classes/redshift-repository.class.ts +146 -0
- package/src/lib/classes/sql-writer.class.spec.ts +20 -0
- package/{dist/esm/lib/classes/sql-writer.class.js → src/lib/classes/sql-writer.class.ts} +29 -16
- package/src/lib/classes/table-migration-base.class.ts +17 -0
- package/src/lib/classes/type-orm-paging-repository.class.ts +22 -0
- package/src/lib/classes/type-orm-repository.class.ts +299 -0
- package/src/lib/constants/many-to-one-relation-options.constant.ts +3 -0
- package/src/lib/decorators/nullable-table-column.decorator.ts +9 -0
- package/src/lib/decorators/primary-table-column.decorator.ts +9 -0
- package/src/lib/decorators/table-column.decorator.ts +9 -0
- package/src/lib/decorators/table.decorator.ts +8 -0
- package/src/lib/functions/data-source-config-factory.function.ts +34 -0
- package/src/lib/functions/data-source-factory.function.ts +10 -0
- package/src/lib/functions/generate-date-query.function.ts +20 -0
- package/src/lib/functions/get-api-type-from-column.function.ts +14 -0
- package/src/lib/functions/get-paging-key.function.ts +3 -0
- package/src/lib/functions/get-skip.function.ts +3 -0
- package/src/lib/functions/remove-falsey-keys.function.ts +8 -0
- package/src/lib/server-typeorm-postgres.module.ts +44 -0
- package/src/lib/types/data-source-options.interface.ts +11 -0
- package/{dist/cjs/lib/types/entity-provider.interface.d.ts → src/lib/types/entity-provider.interface.ts} +2 -2
- package/{dist/esm/lib/types/page-params.interface.d.ts → src/lib/types/page-params.interface.ts} +2 -0
- package/src/lib/types/table-meta.type.ts +4 -0
- package/tsconfig.json +17 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +21 -0
- package/dist/cjs/index.js +0 -44
- package/dist/cjs/lib/classes/column-migration-base.class.d.ts +0 -8
- package/dist/cjs/lib/classes/column-migration-base.class.js +0 -19
- package/dist/cjs/lib/classes/columns-migration-base.class.d.ts +0 -8
- package/dist/cjs/lib/classes/columns-migration-base.class.js +0 -19
- package/dist/cjs/lib/classes/drop-column-migration-base.class.d.ts +0 -8
- package/dist/cjs/lib/classes/drop-column-migration-base.class.js +0 -19
- package/dist/cjs/lib/classes/drop-table-migration-base.class.d.ts +0 -8
- package/dist/cjs/lib/classes/drop-table-migration-base.class.js +0 -19
- package/dist/cjs/lib/classes/index-migration-base.class.d.ts +0 -8
- package/dist/cjs/lib/classes/index-migration-base.class.js +0 -19
- package/dist/cjs/lib/classes/redshift-repository.class.d.ts +0 -29
- package/dist/cjs/lib/classes/redshift-repository.class.js +0 -129
- package/dist/cjs/lib/classes/sql-writer.class.d.ts +0 -14
- package/dist/cjs/lib/classes/sql-writer.class.js +0 -53
- package/dist/cjs/lib/classes/table-migration-base.class.d.ts +0 -8
- package/dist/cjs/lib/classes/table-migration-base.class.js +0 -19
- package/dist/cjs/lib/classes/type-orm-paging-repository.class.d.ts +0 -14
- package/dist/cjs/lib/classes/type-orm-paging-repository.class.js +0 -16
- package/dist/cjs/lib/classes/type-orm-repository.class.d.ts +0 -62
- package/dist/cjs/lib/classes/type-orm-repository.class.js +0 -202
- package/dist/cjs/lib/constants/many-to-one-relation-options.constant.d.ts +0 -2
- package/dist/cjs/lib/constants/many-to-one-relation-options.constant.js +0 -4
- package/dist/cjs/lib/decorators/nullable-table-column.decorator.d.ts +0 -2
- package/dist/cjs/lib/decorators/nullable-table-column.decorator.js +0 -12
- package/dist/cjs/lib/decorators/primary-table-column.decorator.d.ts +0 -2
- package/dist/cjs/lib/decorators/primary-table-column.decorator.js +0 -12
- package/dist/cjs/lib/decorators/table-column.decorator.d.ts +0 -2
- package/dist/cjs/lib/decorators/table-column.decorator.js +0 -12
- package/dist/cjs/lib/decorators/table.decorator.d.ts +0 -3
- package/dist/cjs/lib/decorators/table.decorator.js +0 -11
- package/dist/cjs/lib/functions/data-source-config-factory.function.d.ts +0 -3
- package/dist/cjs/lib/functions/data-source-config-factory.function.js +0 -25
- package/dist/cjs/lib/functions/data-source-factory.function.d.ts +0 -3
- package/dist/cjs/lib/functions/data-source-factory.function.js +0 -7
- package/dist/cjs/lib/functions/generate-date-query.function.d.ts +0 -2
- package/dist/cjs/lib/functions/generate-date-query.function.js +0 -16
- package/dist/cjs/lib/functions/get-api-type-from-column.function.d.ts +0 -2
- package/dist/cjs/lib/functions/get-api-type-from-column.function.js +0 -14
- package/dist/cjs/lib/functions/get-paging-key.function.d.ts +0 -1
- package/dist/cjs/lib/functions/get-paging-key.function.js +0 -6
- package/dist/cjs/lib/functions/get-skip.function.d.ts +0 -1
- package/dist/cjs/lib/functions/get-skip.function.js +0 -6
- package/dist/cjs/lib/functions/remove-falsey-keys.function.d.ts +0 -1
- package/dist/cjs/lib/functions/remove-falsey-keys.function.js +0 -11
- package/dist/cjs/lib/server-typeorm-postgres.module.d.ts +0 -5
- package/dist/cjs/lib/server-typeorm-postgres.module.js +0 -49
- package/dist/cjs/lib/types/data-source-options.interface.d.ts +0 -11
- package/dist/cjs/lib/types/data-source-options.interface.js +0 -2
- package/dist/cjs/lib/types/entity-provider.interface.js +0 -2
- package/dist/cjs/lib/types/page-params.interface.d.ts +0 -4
- package/dist/cjs/lib/types/page-params.interface.js +0 -2
- package/dist/cjs/lib/types/paged-data.interface.js +0 -2
- package/dist/cjs/lib/types/table-meta.type.d.ts +0 -9
- package/dist/cjs/lib/types/table-meta.type.js +0 -2
- package/dist/esm/index.js +0 -44
- package/dist/esm/lib/classes/column-migration-base.class.d.ts +0 -8
- package/dist/esm/lib/classes/column-migration-base.class.js +0 -19
- package/dist/esm/lib/classes/columns-migration-base.class.d.ts +0 -8
- package/dist/esm/lib/classes/columns-migration-base.class.js +0 -19
- package/dist/esm/lib/classes/drop-column-migration-base.class.d.ts +0 -8
- package/dist/esm/lib/classes/drop-column-migration-base.class.js +0 -19
- package/dist/esm/lib/classes/drop-table-migration-base.class.d.ts +0 -8
- package/dist/esm/lib/classes/drop-table-migration-base.class.js +0 -19
- package/dist/esm/lib/classes/index-migration-base.class.d.ts +0 -8
- package/dist/esm/lib/classes/index-migration-base.class.js +0 -19
- package/dist/esm/lib/classes/redshift-repository.class.d.ts +0 -29
- package/dist/esm/lib/classes/redshift-repository.class.js +0 -129
- package/dist/esm/lib/classes/sql-writer.class.d.ts +0 -14
- package/dist/esm/lib/classes/table-migration-base.class.d.ts +0 -8
- package/dist/esm/lib/classes/table-migration-base.class.js +0 -19
- package/dist/esm/lib/classes/type-orm-paging-repository.class.d.ts +0 -14
- package/dist/esm/lib/classes/type-orm-paging-repository.class.js +0 -16
- package/dist/esm/lib/classes/type-orm-repository.class.d.ts +0 -62
- package/dist/esm/lib/classes/type-orm-repository.class.js +0 -202
- package/dist/esm/lib/constants/many-to-one-relation-options.constant.d.ts +0 -2
- package/dist/esm/lib/constants/many-to-one-relation-options.constant.js +0 -4
- package/dist/esm/lib/decorators/nullable-table-column.decorator.d.ts +0 -2
- package/dist/esm/lib/decorators/nullable-table-column.decorator.js +0 -12
- package/dist/esm/lib/decorators/primary-table-column.decorator.d.ts +0 -2
- package/dist/esm/lib/decorators/primary-table-column.decorator.js +0 -12
- package/dist/esm/lib/decorators/table-column.decorator.d.ts +0 -2
- package/dist/esm/lib/decorators/table-column.decorator.js +0 -12
- package/dist/esm/lib/decorators/table.decorator.d.ts +0 -3
- package/dist/esm/lib/decorators/table.decorator.js +0 -11
- package/dist/esm/lib/functions/data-source-config-factory.function.d.ts +0 -3
- package/dist/esm/lib/functions/data-source-config-factory.function.js +0 -25
- package/dist/esm/lib/functions/data-source-factory.function.d.ts +0 -3
- package/dist/esm/lib/functions/data-source-factory.function.js +0 -7
- package/dist/esm/lib/functions/generate-date-query.function.d.ts +0 -2
- package/dist/esm/lib/functions/generate-date-query.function.js +0 -16
- package/dist/esm/lib/functions/get-api-type-from-column.function.d.ts +0 -2
- package/dist/esm/lib/functions/get-api-type-from-column.function.js +0 -14
- package/dist/esm/lib/functions/get-paging-key.function.d.ts +0 -1
- package/dist/esm/lib/functions/get-paging-key.function.js +0 -6
- package/dist/esm/lib/functions/get-skip.function.d.ts +0 -1
- package/dist/esm/lib/functions/get-skip.function.js +0 -6
- package/dist/esm/lib/functions/remove-falsey-keys.function.d.ts +0 -1
- package/dist/esm/lib/functions/remove-falsey-keys.function.js +0 -11
- package/dist/esm/lib/server-typeorm-postgres.module.d.ts +0 -5
- package/dist/esm/lib/server-typeorm-postgres.module.js +0 -49
- package/dist/esm/lib/types/data-source-options.interface.d.ts +0 -11
- package/dist/esm/lib/types/data-source-options.interface.js +0 -2
- package/dist/esm/lib/types/entity-provider.interface.d.ts +0 -9
- package/dist/esm/lib/types/entity-provider.interface.js +0 -2
- package/dist/esm/lib/types/page-params.interface.js +0 -2
- package/dist/esm/lib/types/paged-data.interface.d.ts +0 -6
- package/dist/esm/lib/types/paged-data.interface.js +0 -2
- package/dist/esm/lib/types/table-meta.type.d.ts +0 -9
- package/dist/esm/lib/types/table-meta.type.js +0 -2
- package/dist/types/index.d.ts +0 -28
- package/dist/types/lib/classes/column-migration-base.class.d.ts +0 -8
- package/dist/types/lib/classes/columns-migration-base.class.d.ts +0 -8
- package/dist/types/lib/classes/drop-column-migration-base.class.d.ts +0 -8
- package/dist/types/lib/classes/drop-table-migration-base.class.d.ts +0 -8
- package/dist/types/lib/classes/index-migration-base.class.d.ts +0 -8
- package/dist/types/lib/classes/redshift-repository.class.d.ts +0 -29
- package/dist/types/lib/classes/sql-writer.class.d.ts +0 -14
- package/dist/types/lib/classes/table-migration-base.class.d.ts +0 -8
- package/dist/types/lib/classes/type-orm-paging-repository.class.d.ts +0 -14
- package/dist/types/lib/classes/type-orm-repository.class.d.ts +0 -62
- package/dist/types/lib/constants/many-to-one-relation-options.constant.d.ts +0 -2
- package/dist/types/lib/decorators/nullable-table-column.decorator.d.ts +0 -2
- package/dist/types/lib/decorators/primary-table-column.decorator.d.ts +0 -2
- package/dist/types/lib/decorators/table-column.decorator.d.ts +0 -2
- package/dist/types/lib/decorators/table.decorator.d.ts +0 -3
- package/dist/types/lib/functions/data-source-config-factory.function.d.ts +0 -3
- package/dist/types/lib/functions/data-source-factory.function.d.ts +0 -3
- package/dist/types/lib/functions/generate-date-query.function.d.ts +0 -2
- package/dist/types/lib/functions/get-api-type-from-column.function.d.ts +0 -2
- package/dist/types/lib/functions/get-paging-key.function.d.ts +0 -1
- package/dist/types/lib/functions/get-skip.function.d.ts +0 -1
- package/dist/types/lib/functions/remove-falsey-keys.function.d.ts +0 -1
- package/dist/types/lib/server-typeorm-postgres.module.d.ts +0 -5
- package/dist/types/lib/types/data-source-options.interface.d.ts +0 -11
- package/dist/types/lib/types/entity-provider.interface.d.ts +0 -9
- package/dist/types/lib/types/page-params.interface.d.ts +0 -4
- package/dist/types/lib/types/paged-data.interface.d.ts +0 -6
- package/dist/types/lib/types/table-meta.type.d.ts +0 -9
- /package/{dist/cjs/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.
|