@venturialstd/user 0.0.1
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 +1357 -0
- package/dist/constants/user.constant.d.ts +8 -0
- package/dist/constants/user.constant.d.ts.map +1 -0
- package/dist/constants/user.constant.js +12 -0
- package/dist/constants/user.constant.js.map +1 -0
- package/dist/constants/user.settings.constant.d.ts +8 -0
- package/dist/constants/user.settings.constant.d.ts.map +1 -0
- package/dist/constants/user.settings.constant.js +5 -0
- package/dist/constants/user.settings.constant.js.map +1 -0
- package/dist/decorators/acl.decorator.d.ts +5 -0
- package/dist/decorators/acl.decorator.d.ts.map +1 -0
- package/dist/decorators/acl.decorator.js +11 -0
- package/dist/decorators/acl.decorator.js.map +1 -0
- package/dist/decorators/roles.decorator.d.ts +3 -0
- package/dist/decorators/roles.decorator.d.ts.map +1 -0
- package/dist/decorators/roles.decorator.js +7 -0
- package/dist/decorators/roles.decorator.js.map +1 -0
- package/dist/decorators/user.decorator.d.ts +3 -0
- package/dist/decorators/user.decorator.d.ts.map +1 -0
- package/dist/decorators/user.decorator.js +13 -0
- package/dist/decorators/user.decorator.js.map +1 -0
- package/dist/entities/permission.entity.d.ts +12 -0
- package/dist/entities/permission.entity.d.ts.map +1 -0
- package/dist/entities/permission.entity.js +80 -0
- package/dist/entities/permission.entity.js.map +1 -0
- package/dist/entities/role-permission.entity.d.ts +13 -0
- package/dist/entities/role-permission.entity.d.ts.map +1 -0
- package/dist/entities/role-permission.entity.js +79 -0
- package/dist/entities/role-permission.entity.js.map +1 -0
- package/dist/entities/role.entity.d.ts +15 -0
- package/dist/entities/role.entity.d.ts.map +1 -0
- package/dist/entities/role.entity.js +91 -0
- package/dist/entities/role.entity.js.map +1 -0
- package/dist/entities/user-role.entity.d.ts +14 -0
- package/dist/entities/user-role.entity.d.ts.map +1 -0
- package/dist/entities/user-role.entity.js +82 -0
- package/dist/entities/user-role.entity.js.map +1 -0
- package/dist/entities/user.entity.d.ts +18 -0
- package/dist/entities/user.entity.d.ts.map +1 -0
- package/dist/entities/user.entity.js +129 -0
- package/dist/entities/user.entity.js.map +1 -0
- package/dist/guards/acl.guard.d.ts +16 -0
- package/dist/guards/acl.guard.d.ts.map +1 -0
- package/dist/guards/acl.guard.js +85 -0
- package/dist/guards/acl.guard.js.map +1 -0
- package/dist/guards/user.guard.d.ts +8 -0
- package/dist/guards/user.guard.d.ts.map +1 -0
- package/dist/guards/user.guard.js +39 -0
- package/dist/guards/user.guard.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/services/acl.service.d.ts +39 -0
- package/dist/services/acl.service.d.ts.map +1 -0
- package/dist/services/acl.service.js +307 -0
- package/dist/services/acl.service.js.map +1 -0
- package/dist/services/user.service.d.ts +28 -0
- package/dist/services/user.service.d.ts.map +1 -0
- package/dist/services/user.service.js +173 -0
- package/dist/services/user.service.js.map +1 -0
- package/dist/settings/user.settings.d.ts +8 -0
- package/dist/settings/user.settings.d.ts.map +1 -0
- package/dist/settings/user.settings.js +3 -0
- package/dist/settings/user.settings.js.map +1 -0
- package/dist/user.module.d.ts +12 -0
- package/dist/user.module.d.ts.map +1 -0
- package/dist/user.module.js +48 -0
- package/dist/user.module.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,1357 @@
|
|
|
1
|
+
# @venturialstd/user
|
|
2
|
+
|
|
3
|
+
User Management Module for Venturial - A NestJS module for managing users with comprehensive profile management, settings, and role-based access control.
|
|
4
|
+
|
|
5
|
+
**🎯 Key Design Principle:** This module provides a minimal, extensible base User entity. Extend it with related entities for your specific needs (employment data, contact info, etc.) for optimal performance.
|
|
6
|
+
|
|
7
|
+
## 📋 Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Features](#features)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Module Structure](#module-structure)
|
|
12
|
+
- [Quick Start](#quick-start)
|
|
13
|
+
- [ACL System](#acl-system)
|
|
14
|
+
- [Extending User Statuses](#extending-user-statuses)
|
|
15
|
+
- [Extending the Module](#extending-the-module)
|
|
16
|
+
- [Performance Considerations](#performance-considerations)
|
|
17
|
+
- [Recommended Approach: Related Entities](#recommended-approach-related-entities)
|
|
18
|
+
- [Hybrid Approach](#hybrid-approach-optional-flexibility)
|
|
19
|
+
- [Complete Extension Example](#complete-extension-example)
|
|
20
|
+
- [Extension Methods Comparison](#extension-methods-comparison)
|
|
21
|
+
- [Configuration](#configuration)
|
|
22
|
+
- [Entities](#entities)
|
|
23
|
+
- [API Reference](#api-reference)
|
|
24
|
+
- [Enums & Constants](#enums--constants)
|
|
25
|
+
- [Guards & Decorators](#guards--decorators)
|
|
26
|
+
- [Test Server](#test-server)
|
|
27
|
+
- [NPM Scripts](#npm-scripts)
|
|
28
|
+
- [Usage Examples](#usage-examples)
|
|
29
|
+
- [Performance Best Practices](#performance-best-practices)
|
|
30
|
+
- [Migration Guide](#migration-guide)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## ✨ Features
|
|
35
|
+
|
|
36
|
+
- 👤 **User Management**: Complete CRUD operations for users
|
|
37
|
+
- 📧 **Email Verification**: Email verification system
|
|
38
|
+
- ⚙️ **User Settings**: Configurable user preferences
|
|
39
|
+
- 🎭 **Extensible Statuses**: Define custom user statuses for your application
|
|
40
|
+
- 🔐 **ACL System**: Complete database-driven role and permission management
|
|
41
|
+
- 🛡️ **Guards & Decorators**: Request-level access control
|
|
42
|
+
- 🔄 **TypeORM Integration**: Full database support with migrations
|
|
43
|
+
- 📦 **Fully Typed**: Complete TypeScript support
|
|
44
|
+
- 🧪 **Test Infrastructure**: Standalone test server on port 3003
|
|
45
|
+
- ⚡ **High Performance**: Indexed columns and optimized queries
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 📦 Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install @venturialstd/user
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Peer Dependencies
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"@nestjs/common": "^11.0.11",
|
|
60
|
+
"@nestjs/core": "^11.0.5",
|
|
61
|
+
"@nestjs/typeorm": "^10.0.0",
|
|
62
|
+
"@venturialstd/core": "^1.0.16",
|
|
63
|
+
"class-transformer": "^0.5.1",
|
|
64
|
+
"class-validator": "^0.14.1",
|
|
65
|
+
"typeorm": "^0.3.20"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 📁 Module Structure
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
src/user/
|
|
75
|
+
├── src/
|
|
76
|
+
│ ├── constants/
|
|
77
|
+
│ │ ├── user.constant.ts # USER_STATUS enum
|
|
78
|
+
│ │ └── user.settings.constant.ts # Settings keys and config
|
|
79
|
+
│ ├── decorators/
|
|
80
|
+
│ │ ├── user.decorator.ts # @CurrentUserId(), @CurrentUser()
|
|
81
|
+
│ │ └── acl.decorator.ts # @RequirePermissions(), @RequireAclRoles()
|
|
82
|
+
│ ├── entities/
|
|
83
|
+
│ │ ├── user.entity.ts # User entity
|
|
84
|
+
│ │ ├── role.entity.ts # ACL Role entity
|
|
85
|
+
│ │ ├── permission.entity.ts # ACL Permission entity
|
|
86
|
+
│ │ ├── role-permission.entity.ts # ACL Role-Permission junction
|
|
87
|
+
│ │ └── user-role.entity.ts # ACL User-Role junction
|
|
88
|
+
│ ├── guards/
|
|
89
|
+
│ │ ├── user.guard.ts # UserGuard
|
|
90
|
+
│ │ └── acl.guard.ts # PermissionGuard, AclRoleGuard
|
|
91
|
+
│ ├── services/
|
|
92
|
+
│ │ ├── user.service.ts # UserService
|
|
93
|
+
│ │ └── acl.service.ts # AclService
|
|
94
|
+
│ ├── settings/
|
|
95
|
+
│ │ └── user.settings.ts # UserSettings interface
|
|
96
|
+
│ ├── index.ts # Public API exports
|
|
97
|
+
│ └── user.module.ts # Main module
|
|
98
|
+
├── test/
|
|
99
|
+
│ ├── controllers/
|
|
100
|
+
│ │ └── user-test.controller.ts # Test controller
|
|
101
|
+
│ ├── migrations/ # Database migrations
|
|
102
|
+
│ ├── .env.example # Environment template
|
|
103
|
+
│ ├── data-source.ts # TypeORM data source
|
|
104
|
+
│ ├── main.ts # Test server bootstrap
|
|
105
|
+
│ └── test-app.module.ts # Test module
|
|
106
|
+
├── package.json
|
|
107
|
+
├── tsconfig.json
|
|
108
|
+
└── README.md
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 🚀 Quick Start
|
|
114
|
+
|
|
115
|
+
### 1. Import the Module
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { Module } from '@nestjs/common';
|
|
119
|
+
import { UserModule } from '@venturialstd/user';
|
|
120
|
+
|
|
121
|
+
@Module({
|
|
122
|
+
imports: [
|
|
123
|
+
// Basic import (uses default statuses)
|
|
124
|
+
UserModule.forRoot(),
|
|
125
|
+
|
|
126
|
+
// Or with custom statuses
|
|
127
|
+
UserModule.forRoot({
|
|
128
|
+
allowedStatuses: ['ACTIVE', 'INACTIVE', 'SUSPENDED', 'BANNED'],
|
|
129
|
+
additionalEntities: [UserEmployment, UserProfile],
|
|
130
|
+
}),
|
|
131
|
+
],
|
|
132
|
+
})
|
|
133
|
+
export class AppModule {}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 2. Use in Your Service
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { Injectable } from '@nestjs/common';
|
|
140
|
+
import { UserService, AclService } from '@venturialstd/user';
|
|
141
|
+
|
|
142
|
+
@Injectable()
|
|
143
|
+
export class MyService {
|
|
144
|
+
constructor(
|
|
145
|
+
private readonly userService: UserService,
|
|
146
|
+
private readonly aclService: AclService,
|
|
147
|
+
) {}
|
|
148
|
+
|
|
149
|
+
async getUser(userId: string) {
|
|
150
|
+
return this.userService.getUserById(userId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async assignModeratorRole(userId: string) {
|
|
154
|
+
const role = await this.aclService.getRoleByName('MODERATOR');
|
|
155
|
+
return this.aclService.assignRoleToUser(userId, role.id);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 3. Use Guards and Decorators
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { Controller, Get, UseGuards } from '@nestjs/common';
|
|
164
|
+
import { CurrentUserId, UserGuard, RequirePermissions, PermissionGuard } from '@venturialstd/user';
|
|
165
|
+
|
|
166
|
+
@Controller('profile')
|
|
167
|
+
@UseGuards(UserGuard)
|
|
168
|
+
export class ProfileController {
|
|
169
|
+
@Get()
|
|
170
|
+
async getProfile(@CurrentUserId() userId: string) {
|
|
171
|
+
return { userId };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@Get('admin')
|
|
175
|
+
@RequirePermissions('users.delete', 'content.manage')
|
|
176
|
+
@UseGuards(UserGuard, PermissionGuard)
|
|
177
|
+
async adminAction() {
|
|
178
|
+
return { message: 'Admin action' };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 🔐 ACL System
|
|
186
|
+
|
|
187
|
+
The module includes a complete database-driven Access Control List (ACL) system for managing roles and permissions. See [ACL_SYSTEM.md](./ACL_SYSTEM.md) for comprehensive documentation.
|
|
188
|
+
|
|
189
|
+
### Quick Overview
|
|
190
|
+
|
|
191
|
+
- **Roles**: Named collections of permissions (e.g., ADMIN, MODERATOR)
|
|
192
|
+
- **Permissions**: Granular access rights in `resource.action` format (e.g., `users.delete`, `posts.create`)
|
|
193
|
+
- **Multiple Roles**: Users can have multiple roles with different scopes
|
|
194
|
+
- **Temporal Access**: Roles can expire automatically
|
|
195
|
+
- **Conditional Permissions**: Optional conditions on role-permission assignments
|
|
196
|
+
|
|
197
|
+
### Example Usage
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// Create roles and permissions
|
|
201
|
+
const adminRole = await aclService.createRole('ADMIN', 'Administrator');
|
|
202
|
+
const userPerm = await aclService.createPermission('users', 'delete', 'Delete users');
|
|
203
|
+
|
|
204
|
+
// Assign permission to role
|
|
205
|
+
await aclService.assignPermissionToRole(adminRole.id, userPerm.id);
|
|
206
|
+
|
|
207
|
+
// Assign role to user
|
|
208
|
+
await aclService.assignRoleToUser(userId, adminRole.id);
|
|
209
|
+
|
|
210
|
+
// Check permissions
|
|
211
|
+
const canDelete = await aclService.userHasPermission(userId, 'users.delete');
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 🎭 Extending User Statuses
|
|
217
|
+
|
|
218
|
+
The module provides a flexible system for defining custom user statuses.
|
|
219
|
+
|
|
220
|
+
## 🎭 Extending User Statuses
|
|
221
|
+
|
|
222
|
+
The module provides a flexible system for defining custom user statuses.
|
|
223
|
+
|
|
224
|
+
### Default Values
|
|
225
|
+
|
|
226
|
+
By default, the module includes:
|
|
227
|
+
|
|
228
|
+
**Statuses:**
|
|
229
|
+
- `ACTIVE` - User is active and can use the system
|
|
230
|
+
- `INACTIVE` - User is inactive
|
|
231
|
+
- `SUSPENDED` - User has been suspended
|
|
232
|
+
- `PENDING_VERIFICATION` - User email needs verification
|
|
233
|
+
|
|
234
|
+
### Defining Custom Statuses
|
|
235
|
+
|
|
236
|
+
Create an enum for your application-specific statuses:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Define custom statuses
|
|
240
|
+
export enum AppUserStatus {
|
|
241
|
+
ACTIVE = 'ACTIVE',
|
|
242
|
+
INACTIVE = 'INACTIVE',
|
|
243
|
+
SUSPENDED = 'SUSPENDED',
|
|
244
|
+
PENDING_VERIFICATION = 'PENDING_VERIFICATION',
|
|
245
|
+
ON_HOLD = 'ON_HOLD',
|
|
246
|
+
BANNED = 'BANNED',
|
|
247
|
+
DELETED = 'DELETED',
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@Module({
|
|
251
|
+
imports: [
|
|
252
|
+
UserModule.forRoot({
|
|
253
|
+
allowedStatuses: Object.values(AppUserStatus),
|
|
254
|
+
}),
|
|
255
|
+
],
|
|
256
|
+
})
|
|
257
|
+
export class AppModule {}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Managing Statuses
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
@Injectable()
|
|
264
|
+
export class UserManagementService {
|
|
265
|
+
constructor(private userService: UserService) {}
|
|
266
|
+
|
|
267
|
+
// Set user status with validation
|
|
268
|
+
async banUser(userId: string) {
|
|
269
|
+
return this.userService.setStatus(userId, AppUserStatus.BANNED);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Get users by status
|
|
273
|
+
async getBannedUsers() {
|
|
274
|
+
return this.userService.getUsersByStatus(AppUserStatus.BANNED);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Get all allowed statuses for your application
|
|
278
|
+
getAllStatuses() {
|
|
279
|
+
return this.userService.getAllowedStatuses();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Validation
|
|
285
|
+
|
|
286
|
+
The module automatically validates statuses:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// ✅ Valid - status is in allowedStatuses
|
|
290
|
+
await userService.setStatus(userId, AppUserStatus.BANNED);
|
|
291
|
+
|
|
292
|
+
// ❌ Invalid - throws BadRequestException
|
|
293
|
+
await userService.setStatus(userId, 'INVALID_STATUS');
|
|
294
|
+
// Error: Invalid status "INVALID_STATUS". Allowed statuses: ACTIVE, INACTIVE, SUSPENDED, ...
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 🔧 Extending the Module
|
|
300
|
+
|
|
301
|
+
### Performance Considerations
|
|
302
|
+
|
|
303
|
+
**⚠️ Important:** The `metadata` and `settings` JSONB fields are available but **NOT recommended for filterable attributes** due to poor query performance.
|
|
304
|
+
|
|
305
|
+
**Performance Comparison:**
|
|
306
|
+
```sql
|
|
307
|
+
-- ❌ SLOW: JSONB query (full table scan)
|
|
308
|
+
SELECT * FROM "user" WHERE metadata->>'department' = 'Engineering';
|
|
309
|
+
-- 1M records: ~50 seconds
|
|
310
|
+
|
|
311
|
+
-- ⚡ FAST: Indexed column query
|
|
312
|
+
SELECT * FROM user_employment WHERE department = 'Engineering';
|
|
313
|
+
-- 1M records: ~10 milliseconds
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Result: 5,000x faster with proper indexes!**
|
|
317
|
+
|
|
318
|
+
### Recommended Approach: Related Entities
|
|
319
|
+
|
|
320
|
+
**Always use related entities for any data you need to query or filter.**
|
|
321
|
+
|
|
322
|
+
#### Step 1: Create Your Extended Entity
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, Index } from 'typeorm';
|
|
326
|
+
import { User } from '@venturialstd/user';
|
|
327
|
+
|
|
328
|
+
@Entity('user_employment')
|
|
329
|
+
@Index(['department', 'isActive']) // Composite index for common queries
|
|
330
|
+
export class UserEmployment {
|
|
331
|
+
@PrimaryGeneratedColumn('uuid')
|
|
332
|
+
id: string;
|
|
333
|
+
|
|
334
|
+
@Column()
|
|
335
|
+
@Index() // Fast lookups by userId
|
|
336
|
+
userId: string;
|
|
337
|
+
|
|
338
|
+
@OneToOne(() => User)
|
|
339
|
+
@JoinColumn({ name: 'userId' })
|
|
340
|
+
user: User;
|
|
341
|
+
|
|
342
|
+
// ⚡ Indexed fields for fast filtering
|
|
343
|
+
@Column({ unique: true })
|
|
344
|
+
employeeId: string;
|
|
345
|
+
|
|
346
|
+
@Column()
|
|
347
|
+
@Index() // Fast department filtering
|
|
348
|
+
department: string;
|
|
349
|
+
|
|
350
|
+
@Column()
|
|
351
|
+
jobTitle: string;
|
|
352
|
+
|
|
353
|
+
@Column({ nullable: true })
|
|
354
|
+
@Index() // Fast manager lookups
|
|
355
|
+
managerId: string;
|
|
356
|
+
|
|
357
|
+
@Column({ type: 'date' })
|
|
358
|
+
hireDate: Date;
|
|
359
|
+
|
|
360
|
+
@Column({ default: true })
|
|
361
|
+
@Index() // Fast active status queries
|
|
362
|
+
isActive: boolean;
|
|
363
|
+
|
|
364
|
+
@Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
|
|
365
|
+
createdAt: Date;
|
|
366
|
+
|
|
367
|
+
@Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
|
|
368
|
+
updatedAt: Date;
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### Step 2: Create Service with Efficient Queries
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { Injectable } from '@nestjs/common';
|
|
376
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
377
|
+
import { Repository } from 'typeorm';
|
|
378
|
+
import { UserService } from '@venturialstd/user';
|
|
379
|
+
import { UserEmployment } from './entities/user-employment.entity';
|
|
380
|
+
|
|
381
|
+
@Injectable()
|
|
382
|
+
export class EmployeeService {
|
|
383
|
+
constructor(
|
|
384
|
+
@InjectRepository(UserEmployment)
|
|
385
|
+
private employmentRepo: Repository<UserEmployment>,
|
|
386
|
+
private userService: UserService,
|
|
387
|
+
) {}
|
|
388
|
+
|
|
389
|
+
async createEmployee(data: {
|
|
390
|
+
firstname: string;
|
|
391
|
+
lastname: string;
|
|
392
|
+
email: string;
|
|
393
|
+
department: string;
|
|
394
|
+
employeeId: string;
|
|
395
|
+
jobTitle: string;
|
|
396
|
+
}) {
|
|
397
|
+
// Create base user
|
|
398
|
+
const user = await this.userService.createUser(
|
|
399
|
+
data.firstname,
|
|
400
|
+
data.lastname,
|
|
401
|
+
data.email,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// Create employment record
|
|
405
|
+
const employment = this.employmentRepo.create({
|
|
406
|
+
userId: user.id,
|
|
407
|
+
employeeId: data.employeeId,
|
|
408
|
+
department: data.department,
|
|
409
|
+
jobTitle: data.jobTitle,
|
|
410
|
+
hireDate: new Date(),
|
|
411
|
+
isActive: true,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
await this.employmentRepo.save(employment);
|
|
415
|
+
return this.getEmployeeWithUser(user.id);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ⚡ FAST: Uses department index
|
|
419
|
+
async getEmployeesByDepartment(department: string) {
|
|
420
|
+
return this.employmentRepo.find({
|
|
421
|
+
where: { department, isActive: true },
|
|
422
|
+
relations: ['user'],
|
|
423
|
+
order: { createdAt: 'DESC' },
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ⚡ FAST: Complex query with proper indexes
|
|
428
|
+
async searchEmployees(searchTerm: string, department?: string) {
|
|
429
|
+
const query = this.employmentRepo
|
|
430
|
+
.createQueryBuilder('emp')
|
|
431
|
+
.leftJoinAndSelect('emp.user', 'user')
|
|
432
|
+
.where('emp.isActive = :active', { active: true });
|
|
433
|
+
|
|
434
|
+
if (department) {
|
|
435
|
+
query.andWhere('emp.department = :dept', { dept: department });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
query.andWhere(
|
|
439
|
+
'(user.firstname ILIKE :search OR user.lastname ILIKE :search OR emp.employeeId ILIKE :search)',
|
|
440
|
+
{ search: `%${searchTerm}%` },
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
return query.getMany();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async getEmployeeWithUser(userId: string) {
|
|
447
|
+
return this.employmentRepo.findOne({
|
|
448
|
+
where: { userId },
|
|
449
|
+
relations: ['user'],
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
#### Step 3: Register with UserModule
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
import { Module } from '@nestjs/common';
|
|
459
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
460
|
+
import { UserModule } from '@venturialstd/user';
|
|
461
|
+
import { UserEmployment } from './entities/user-employment.entity';
|
|
462
|
+
import { EmployeeService } from './services/employee.service';
|
|
463
|
+
|
|
464
|
+
@Module({
|
|
465
|
+
imports: [
|
|
466
|
+
// Register your entities with UserModule
|
|
467
|
+
UserModule.forRoot({
|
|
468
|
+
additionalEntities: [UserEmployment],
|
|
469
|
+
}),
|
|
470
|
+
// Also register for your service
|
|
471
|
+
TypeOrmModule.forFeature([UserEmployment]),
|
|
472
|
+
],
|
|
473
|
+
providers: [EmployeeService],
|
|
474
|
+
exports: [EmployeeService],
|
|
475
|
+
})
|
|
476
|
+
export class EmployeeModule {}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### Benefits:
|
|
480
|
+
- ⚡ **100x-5000x faster queries** with proper indexes
|
|
481
|
+
- ✅ Database constraints (unique, foreign keys)
|
|
482
|
+
- ✅ Full TypeScript type safety
|
|
483
|
+
- ✅ Complex queries (JOINs, aggregations)
|
|
484
|
+
- ✅ Scales to millions of records
|
|
485
|
+
|
|
486
|
+
### Hybrid Approach (Optional Flexibility)
|
|
487
|
+
|
|
488
|
+
If you need **some** flexibility for truly dynamic, non-queryable fields:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
@Entity('user_employment')
|
|
492
|
+
export class UserEmployment {
|
|
493
|
+
// ... indexed columns for queryable fields ...
|
|
494
|
+
|
|
495
|
+
@Column()
|
|
496
|
+
@Index()
|
|
497
|
+
department: string; // ⚡ Fast queries
|
|
498
|
+
|
|
499
|
+
@Column({ unique: true })
|
|
500
|
+
employeeId: string; // ⚡ Fast lookups
|
|
501
|
+
|
|
502
|
+
// 📦 FLEXIBLE: For truly dynamic, non-queryable data only
|
|
503
|
+
@Column({ type: 'jsonb', nullable: true })
|
|
504
|
+
additionalInfo: {
|
|
505
|
+
shirtSize?: string;
|
|
506
|
+
parkingSpot?: string;
|
|
507
|
+
badgeNumber?: string;
|
|
508
|
+
emergencyContact?: {
|
|
509
|
+
name: string;
|
|
510
|
+
phone: string;
|
|
511
|
+
};
|
|
512
|
+
[key: string]: any;
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Complete Extension Example
|
|
518
|
+
|
|
519
|
+
See the `examples/` directory for complete working examples:
|
|
520
|
+
- `user-employee-profile.entity.ts` - Entity with proper indexes
|
|
521
|
+
- `user-employee.service.ts` - Service with efficient queries
|
|
522
|
+
- `user-employee.module.ts` - Module configuration
|
|
523
|
+
|
|
524
|
+
### Extension Methods Comparison
|
|
525
|
+
|
|
526
|
+
| Method | Query Performance | Flexibility | Type Safety | Best For |
|
|
527
|
+
|--------|------------------|-------------|-------------|----------|
|
|
528
|
+
| **Related Entities** ✅ | ⚡⚡⚡ Excellent | ⭐⭐ Medium | ⭐⭐⭐ Strong | **Production (99% of cases)** |
|
|
529
|
+
| **Hybrid (Entities + JSONB)** | ⚡⚡⭐ Good | ⭐⭐⭐ High | ⭐⭐⭐ Strong | Some flexibility needed |
|
|
530
|
+
| **Pure JSONB** ❌ | ⚠️ Poor | ⭐⭐⭐ High | ⭐ Weak | **Avoid for queryable data** |
|
|
531
|
+
| **EAV Pattern** ❌ | ⚠️ Very Poor | ⭐⭐⭐ High | ⭐ Weak | **Not recommended** |
|
|
532
|
+
|
|
533
|
+
**Decision Tree:**
|
|
534
|
+
- Need to filter/query this data? → **Use Related Entity** ✅
|
|
535
|
+
- Just storing UI preferences? → **Can use JSONB** 📝
|
|
536
|
+
- Truly unknown runtime schema? → **Consider Hybrid approach**
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## 📊 Entities
|
|
541
|
+
|
|
542
|
+
### User Entity
|
|
543
|
+
|
|
544
|
+
The main `User` entity represents a user in the system:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
{
|
|
548
|
+
id: string; // UUID primary key
|
|
549
|
+
firstname: string; // User's first name
|
|
550
|
+
lastname: string; // User's last name
|
|
551
|
+
email: string; // Unique email address
|
|
552
|
+
phone?: string; // Optional phone number
|
|
553
|
+
avatar?: string; // Optional avatar URL
|
|
554
|
+
timezone?: string; // User's timezone
|
|
555
|
+
locale?: string; // User's locale (e.g., 'en-US')
|
|
556
|
+
status?: string; // User status (extensible, e.g., 'ACTIVE', 'SUSPENDED')
|
|
557
|
+
role?: string; // User role (extensible, e.g., 'ADMIN', 'USER', 'MODERATOR')
|
|
558
|
+
isActive: boolean; // Account active flag
|
|
559
|
+
isEmailVerified: boolean; // Email verification status
|
|
560
|
+
settings?: Record<string, unknown>; // User settings (JSONB) - for UI preferences only
|
|
561
|
+
metadata?: Record<string, unknown>; // Additional metadata (JSONB) - for non-queryable data only
|
|
562
|
+
createdAt: Date; // Creation timestamp
|
|
563
|
+
updatedAt: Date; // Last update timestamp
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**⚠️ Important Notes:**
|
|
568
|
+
|
|
569
|
+
1. **`role` and `status` are extensible** - Define your own values via `UserModule.forRoot()` configuration
|
|
570
|
+
2. **Indexed fields** - Both `role` and `status` have database indexes for fast queries
|
|
571
|
+
3. **JSONB fields (`settings`, `metadata`)** - Should only be used for data you'll never query or filter on
|
|
572
|
+
4. **For business data** - Use related entities instead of JSONB (see Extensibility section)
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## 🔌 API Reference
|
|
577
|
+
|
|
578
|
+
### UserService
|
|
579
|
+
|
|
580
|
+
#### `createUser(firstname, lastname, email, phone?, avatar?, timezone?, locale?)`
|
|
581
|
+
Creates a new user with validation.
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
const user = await userService.createUser(
|
|
585
|
+
'John',
|
|
586
|
+
'Doe',
|
|
587
|
+
'john@example.com',
|
|
588
|
+
'+1234567890',
|
|
589
|
+
'https://avatar.url',
|
|
590
|
+
'America/New_York',
|
|
591
|
+
'en-US'
|
|
592
|
+
);
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
#### `getUserById(userId: string)`
|
|
596
|
+
Gets a user by their ID.
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
const user = await userService.getUserById('user-uuid');
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
#### `getUserByEmail(email: string)`
|
|
603
|
+
Gets a user by their email address.
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
const user = await userService.getUserByEmail('john@example.com');
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
#### `updateUserProfile(userId, updates)`
|
|
610
|
+
Updates user profile fields.
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
const user = await userService.updateUserProfile('user-uuid', {
|
|
614
|
+
firstname: 'Jane',
|
|
615
|
+
timezone: 'Europe/London',
|
|
616
|
+
});
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
#### `updateUserSettings(userId, settings)`
|
|
620
|
+
Updates user settings.
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
const user = await userService.updateUserSettings('user-uuid', {
|
|
624
|
+
notificationsEnabled: true,
|
|
625
|
+
theme: 'dark',
|
|
626
|
+
});
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
#### `updateUserMetadata(userId, metadata)`
|
|
630
|
+
Updates user metadata.
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
const user = await userService.updateUserMetadata('user-uuid', {
|
|
634
|
+
role: 'ADMIN',
|
|
635
|
+
department: 'Engineering',
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
#### `setUserStatus(userId, isActive)`
|
|
640
|
+
Activates or deactivates a user.
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
const user = await userService.setUserStatus('user-uuid', false);
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
#### `verifyEmail(userId)`
|
|
647
|
+
Marks a user's email as verified.
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
const user = await userService.verifyEmail('user-uuid');
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
#### `getActiveUsers()`
|
|
654
|
+
Gets all active users.
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
const users = await userService.getActiveUsers();
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
#### `getVerifiedUsers()`
|
|
661
|
+
Gets all users with verified emails.
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
const users = await userService.getVerifiedUsers();
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
#### `setRole(userId: string, role: string)`
|
|
668
|
+
Sets a user's role with validation.
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
const user = await userService.setRole('user-uuid', 'MODERATOR');
|
|
672
|
+
// Validates against allowedRoles from UserModule configuration
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
#### `setStatus(userId: string, status: string)`
|
|
676
|
+
Sets a user's status with validation.
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
const user = await userService.setStatus('user-uuid', 'BANNED');
|
|
680
|
+
// Validates against allowedStatuses from UserModule configuration
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
#### `getUsersByRole(role: string)`
|
|
684
|
+
Gets all users with a specific role.
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
const moderators = await userService.getUsersByRole('MODERATOR');
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
#### `getUsersByStatus(status: string)`
|
|
691
|
+
Gets all users with a specific status.
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
const bannedUsers = await userService.getUsersByStatus('BANNED');
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
#### `getAllowedRoles()`
|
|
698
|
+
Gets the list of allowed roles for your application.
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
const roles = userService.getAllowedRoles();
|
|
702
|
+
// Returns: ['ADMIN', 'USER', 'MODERATOR', ...]
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
#### `getAllowedStatuses()`
|
|
706
|
+
Gets the list of allowed statuses for your application.
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
const statuses = userService.getAllowedStatuses();
|
|
710
|
+
// Returns: ['ACTIVE', 'INACTIVE', 'SUSPENDED', ...]
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
#### `isValidRole(role: string)`
|
|
714
|
+
Checks if a role is valid for your application.
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
if (userService.isValidRole('MODERATOR')) {
|
|
718
|
+
// Role is valid
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
#### `isValidStatus(status: string)`
|
|
723
|
+
Checks if a status is valid for your application.
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
if (userService.isValidStatus('BANNED')) {
|
|
727
|
+
// Status is valid
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## 🔐 Enums & Constants
|
|
734
|
+
|
|
735
|
+
### USER_STATUS
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
enum USER_STATUS {
|
|
739
|
+
ACTIVE = 'ACTIVE',
|
|
740
|
+
INACTIVE = 'INACTIVE',
|
|
741
|
+
SUSPENDED = 'SUSPENDED',
|
|
742
|
+
PENDING_VERIFICATION = 'PENDING_VERIFICATION',
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
### USER_ROLE
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
enum USER_ROLE {
|
|
750
|
+
ADMIN = 'ADMIN',
|
|
751
|
+
USER = 'USER',
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### USER_SETTINGS_KEY
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
const USER_SETTINGS_KEY = 'USER_SETTINGS';
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
## 🛡️ Guards & Decorators
|
|
764
|
+
|
|
765
|
+
### Guards
|
|
766
|
+
|
|
767
|
+
#### `UserGuard`
|
|
768
|
+
Ensures the user is authenticated and active.
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
@UseGuards(UserGuard)
|
|
772
|
+
@Get()
|
|
773
|
+
async getProfile(@CurrentUserId() userId: string) {
|
|
774
|
+
return this.userService.getUserById(userId);
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
#### `UserRoleGuard`
|
|
779
|
+
Enforces role-based access control.
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
@RequireRoles(USER_ROLE.ADMIN)
|
|
783
|
+
@UseGuards(UserGuard, UserRoleGuard)
|
|
784
|
+
@Delete(':id')
|
|
785
|
+
async deleteUser(@Param('id') id: string) {
|
|
786
|
+
return this.userService.setUserStatus(id, false);
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### Decorators
|
|
791
|
+
|
|
792
|
+
#### `@CurrentUserId()`
|
|
793
|
+
Extracts the current user ID from the request.
|
|
794
|
+
|
|
795
|
+
```typescript
|
|
796
|
+
@Get()
|
|
797
|
+
async getData(@CurrentUserId() userId: string) {
|
|
798
|
+
return this.service.getUserData(userId);
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
#### `@CurrentUser()`
|
|
803
|
+
Extracts the full user object from the request.
|
|
804
|
+
|
|
805
|
+
```typescript
|
|
806
|
+
@Get()
|
|
807
|
+
async getData(@CurrentUser() user: User) {
|
|
808
|
+
return { user };
|
|
809
|
+
}
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
#### `@RequireRoles(...roles)`
|
|
813
|
+
Specifies required roles for a route.
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
@RequireRoles(USER_ROLE.ADMIN)
|
|
817
|
+
@UseGuards(UserGuard, UserRoleGuard)
|
|
818
|
+
@Delete(':id')
|
|
819
|
+
async delete(@Param('id') id: string) {
|
|
820
|
+
// Only admins can access
|
|
821
|
+
}
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
---
|
|
825
|
+
|
|
826
|
+
## 🧪 Test Server
|
|
827
|
+
|
|
828
|
+
The module includes a standalone test server for development and testing:
|
|
829
|
+
|
|
830
|
+
### Start Test Server
|
|
831
|
+
|
|
832
|
+
```bash
|
|
833
|
+
cd src/user
|
|
834
|
+
npm run test:dev
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
The server starts on port **3003** (configurable via `.env`).
|
|
838
|
+
|
|
839
|
+
### Watch Mode
|
|
840
|
+
|
|
841
|
+
```bash
|
|
842
|
+
npm run test:watch
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
### Environment Setup
|
|
846
|
+
|
|
847
|
+
1. Copy `.env.example` to `.env`:
|
|
848
|
+
```bash
|
|
849
|
+
cp test/.env.example test/.env
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
2. Configure your database settings in `test/.env`
|
|
853
|
+
|
|
854
|
+
### Available Test Endpoints
|
|
855
|
+
|
|
856
|
+
```
|
|
857
|
+
POST /users - Create user
|
|
858
|
+
GET /users - Get all users
|
|
859
|
+
GET /users/active - Get active users
|
|
860
|
+
GET /users/verified - Get verified users
|
|
861
|
+
GET /users/email/:email - Get user by email
|
|
862
|
+
GET /users/:id - Get user by ID
|
|
863
|
+
PUT /users/:id/profile - Update user profile
|
|
864
|
+
PUT /users/:id/settings - Update user settings
|
|
865
|
+
PUT /users/:id/metadata - Update user metadata
|
|
866
|
+
PUT /users/:id/status - Update user status
|
|
867
|
+
PUT /users/:id/verify-email - Verify user email
|
|
868
|
+
DELETE /users/:id - Delete user
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
## 📝 NPM Scripts
|
|
874
|
+
|
|
875
|
+
```json
|
|
876
|
+
{
|
|
877
|
+
"build": "tsc -p tsconfig.json",
|
|
878
|
+
"prepublishOnly": "npm run build",
|
|
879
|
+
"release:patch": "npm run build && npm version patch --no-git-tag-version && npm publish",
|
|
880
|
+
"test:dev": "ts-node -r tsconfig-paths/register test/main.ts",
|
|
881
|
+
"test:watch": "nodemon --watch src --watch test --ext ts --exec npm run test:dev",
|
|
882
|
+
"migration:generate": "ts-node node_modules/.bin/typeorm migration:generate -d test/data-source.ts test/migrations/$npm_config_name",
|
|
883
|
+
"migration:run": "ts-node node_modules/.bin/typeorm migration:run -d test/data-source.ts",
|
|
884
|
+
"migration:revert": "ts-node node_modules/.bin/typeorm migration:revert -d test/data-source.ts"
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## 💡 Usage Examples
|
|
891
|
+
|
|
892
|
+
### Example 1: Create and Verify User
|
|
893
|
+
|
|
894
|
+
```typescript
|
|
895
|
+
// Create a new user
|
|
896
|
+
const user = await userService.createUser(
|
|
897
|
+
'Jane',
|
|
898
|
+
'Smith',
|
|
899
|
+
'jane@example.com',
|
|
900
|
+
'+1987654321',
|
|
901
|
+
null,
|
|
902
|
+
'America/Los_Angeles',
|
|
903
|
+
'en-US'
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
// Verify email
|
|
907
|
+
await userService.verifyEmail(user.id);
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
### Example 2: Update User Profile
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
const updatedUser = await userService.updateUserProfile(userId, {
|
|
914
|
+
firstname: 'Janet',
|
|
915
|
+
timezone: 'Europe/Paris',
|
|
916
|
+
locale: 'fr-FR',
|
|
917
|
+
});
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### Example 3: Manage User Settings
|
|
921
|
+
|
|
922
|
+
```typescript
|
|
923
|
+
// Update settings
|
|
924
|
+
await userService.updateUserSettings(userId, {
|
|
925
|
+
notificationsEnabled: true,
|
|
926
|
+
emailNotifications: false,
|
|
927
|
+
theme: 'dark',
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// Get user with settings
|
|
931
|
+
const user = await userService.getUserById(userId);
|
|
932
|
+
console.log(user.settings);
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### Example 4: Role-Based Access
|
|
936
|
+
|
|
937
|
+
```typescript
|
|
938
|
+
// Set user role in metadata
|
|
939
|
+
await userService.updateUserMetadata(userId, {
|
|
940
|
+
role: USER_ROLE.ADMIN,
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// Controller with role guard
|
|
944
|
+
@Controller('admin')
|
|
945
|
+
export class AdminController {
|
|
946
|
+
@RequireRoles(USER_ROLE.ADMIN)
|
|
947
|
+
@UseGuards(UserGuard, UserRoleGuard)
|
|
948
|
+
@Get('dashboard')
|
|
949
|
+
async getDashboard(@CurrentUser() user: User) {
|
|
950
|
+
return { admin: user };
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### Example 5: Filter Users
|
|
956
|
+
|
|
957
|
+
```typescript
|
|
958
|
+
// Get all active users
|
|
959
|
+
const activeUsers = await userService.getActiveUsers();
|
|
960
|
+
|
|
961
|
+
// Get verified users
|
|
962
|
+
const verifiedUsers = await userService.getVerifiedUsers();
|
|
963
|
+
|
|
964
|
+
// Find by email
|
|
965
|
+
const user = await userService.getUserByEmail('john@example.com');
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
---
|
|
969
|
+
|
|
970
|
+
## 🔧 Database Migrations
|
|
971
|
+
|
|
972
|
+
### Generate a Migration
|
|
973
|
+
|
|
974
|
+
```bash
|
|
975
|
+
npm run migration:generate --name=AddUserFields
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
### Run Migrations
|
|
979
|
+
|
|
980
|
+
```bash
|
|
981
|
+
npm run migration:run
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
### Revert Migration
|
|
985
|
+
|
|
986
|
+
```bash
|
|
987
|
+
npm run migration:revert
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## � Best Practices
|
|
993
|
+
|
|
994
|
+
### 1. Always Use Indexes for Queryable Fields
|
|
995
|
+
|
|
996
|
+
```typescript
|
|
997
|
+
// ✅ GOOD: Indexed columns
|
|
998
|
+
@Entity('user_profile')
|
|
999
|
+
export class UserProfile {
|
|
1000
|
+
@Column() @Index() // Fast lookups
|
|
1001
|
+
userId: string;
|
|
1002
|
+
|
|
1003
|
+
@Column() @Index() // Fast filtering
|
|
1004
|
+
country: string;
|
|
1005
|
+
|
|
1006
|
+
@Column({ unique: true }) // Automatically indexed
|
|
1007
|
+
taxId: string;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// ❌ BAD: JSONB for queryable data
|
|
1011
|
+
user.metadata = {
|
|
1012
|
+
country: 'US', // Will be slow to query!
|
|
1013
|
+
taxId: '123' // No unique constraint possible!
|
|
1014
|
+
};
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### 2. Use JSONB Only for Display Data
|
|
1018
|
+
|
|
1019
|
+
```typescript
|
|
1020
|
+
// ✅ GOOD: Non-queryable UI preferences
|
|
1021
|
+
user.settings = {
|
|
1022
|
+
theme: 'dark',
|
|
1023
|
+
language: 'en',
|
|
1024
|
+
sidebarCollapsed: true
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
// ✅ GOOD: Audit/log data you never filter on
|
|
1028
|
+
user.metadata = {
|
|
1029
|
+
lastLoginIp: '192.168.1.1',
|
|
1030
|
+
userAgent: 'Mozilla/5.0...',
|
|
1031
|
+
registrationSource: 'mobile_app'
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
// ❌ BAD: Business data you need to query
|
|
1035
|
+
user.metadata = {
|
|
1036
|
+
subscriptionTier: 'premium', // Use UserSubscription entity!
|
|
1037
|
+
department: 'Engineering', // Use UserEmployment entity!
|
|
1038
|
+
isActive: true // Use column on User entity!
|
|
1039
|
+
};
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
### 3. Create Composite Indexes for Common Queries
|
|
1043
|
+
|
|
1044
|
+
```typescript
|
|
1045
|
+
@Entity('user_subscription')
|
|
1046
|
+
@Index(['userId', 'isActive']) // For userId + status queries
|
|
1047
|
+
@Index(['plan', 'expiresAt']) // For plan + expiration queries
|
|
1048
|
+
export class UserSubscription {
|
|
1049
|
+
@Column() userId: string;
|
|
1050
|
+
@Column() plan: string;
|
|
1051
|
+
@Column() isActive: boolean;
|
|
1052
|
+
@Column() expiresAt: Date;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Now these queries are super fast:
|
|
1056
|
+
await repo.find({
|
|
1057
|
+
where: { userId: 'abc-123', isActive: true } // Uses composite index
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
await repo.find({
|
|
1061
|
+
where: {
|
|
1062
|
+
plan: 'premium',
|
|
1063
|
+
expiresAt: MoreThan(new Date())
|
|
1064
|
+
} // Uses composite index
|
|
1065
|
+
});
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
### 4. Use TypeORM QueryBuilder for Complex Queries
|
|
1069
|
+
|
|
1070
|
+
```typescript
|
|
1071
|
+
// ✅ Complex query with proper indexes
|
|
1072
|
+
const result = await employmentRepo
|
|
1073
|
+
.createQueryBuilder('emp')
|
|
1074
|
+
.leftJoinAndSelect('emp.user', 'user')
|
|
1075
|
+
.where('emp.department = :dept', { dept: 'Engineering' })
|
|
1076
|
+
.andWhere('emp.isActive = :active', { active: true })
|
|
1077
|
+
.andWhere('user.isEmailVerified = :verified', { verified: true })
|
|
1078
|
+
.orderBy('emp.hireDate', 'DESC')
|
|
1079
|
+
.limit(50)
|
|
1080
|
+
.getMany();
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
### 5. Validate Data at the Application Layer
|
|
1084
|
+
|
|
1085
|
+
```typescript
|
|
1086
|
+
// Use DTOs with class-validator
|
|
1087
|
+
export class CreateEmployeeDto {
|
|
1088
|
+
@IsString()
|
|
1089
|
+
@MinLength(2)
|
|
1090
|
+
firstname: string;
|
|
1091
|
+
|
|
1092
|
+
@IsEmail()
|
|
1093
|
+
email: string;
|
|
1094
|
+
|
|
1095
|
+
@IsEnum(['Engineering', 'Sales', 'Marketing'])
|
|
1096
|
+
department: string;
|
|
1097
|
+
|
|
1098
|
+
@IsPositive()
|
|
1099
|
+
@Max(1000000)
|
|
1100
|
+
salary: number;
|
|
1101
|
+
}
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
### 6. Handle Relations Efficiently
|
|
1105
|
+
|
|
1106
|
+
```typescript
|
|
1107
|
+
// ✅ GOOD: Eager load when you know you need it
|
|
1108
|
+
const employees = await repo.find({
|
|
1109
|
+
where: { department: 'Engineering' },
|
|
1110
|
+
relations: ['user'] // Load user in same query
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
// ✅ GOOD: Load separately for optional data
|
|
1114
|
+
const employee = await repo.findOne({ where: { userId } });
|
|
1115
|
+
if (needUserDetails) {
|
|
1116
|
+
employee.user = await userService.findOne(userId);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ❌ BAD: N+1 query problem
|
|
1120
|
+
const employees = await repo.find({ where: { department: 'Engineering' } });
|
|
1121
|
+
for (const emp of employees) {
|
|
1122
|
+
emp.user = await userService.findOne(emp.userId); // Queries in loop!
|
|
1123
|
+
}
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### 7. Use Transactions for Multi-Entity Operations
|
|
1127
|
+
|
|
1128
|
+
```typescript
|
|
1129
|
+
async createEmployeeWithProfile(data: CreateEmployeeDto) {
|
|
1130
|
+
return this.dataSource.transaction(async (manager) => {
|
|
1131
|
+
// Create user
|
|
1132
|
+
const user = await manager.save(User, {
|
|
1133
|
+
firstname: data.firstname,
|
|
1134
|
+
lastname: data.lastname,
|
|
1135
|
+
email: data.email
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// Create employment record
|
|
1139
|
+
const employment = await manager.save(UserEmployment, {
|
|
1140
|
+
userId: user.id,
|
|
1141
|
+
department: data.department,
|
|
1142
|
+
salary: data.salary
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// If anything fails, both are rolled back
|
|
1146
|
+
return { user, employment };
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
### 8. Document Your Extension Entities
|
|
1152
|
+
|
|
1153
|
+
```typescript
|
|
1154
|
+
/**
|
|
1155
|
+
* UserEmployment Entity
|
|
1156
|
+
*
|
|
1157
|
+
* Stores employee-specific information for users who are employees.
|
|
1158
|
+
* Related to User entity via userId.
|
|
1159
|
+
*
|
|
1160
|
+
* Indexes:
|
|
1161
|
+
* - userId: For fast user lookups
|
|
1162
|
+
* - department: For filtering by department
|
|
1163
|
+
* - (department, isActive): For active employee queries by department
|
|
1164
|
+
*
|
|
1165
|
+
* @example
|
|
1166
|
+
* const emp = await employmentRepo.findOne({
|
|
1167
|
+
* where: { userId: 'abc-123' }
|
|
1168
|
+
* });
|
|
1169
|
+
*/
|
|
1170
|
+
@Entity('user_employment')
|
|
1171
|
+
@Index(['department', 'isActive'])
|
|
1172
|
+
export class UserEmployment {
|
|
1173
|
+
// ...
|
|
1174
|
+
}
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
### 9. Plan for Data Growth
|
|
1178
|
+
|
|
1179
|
+
```typescript
|
|
1180
|
+
// Consider pagination for large result sets
|
|
1181
|
+
async getEmployeesByDepartment(
|
|
1182
|
+
department: string,
|
|
1183
|
+
page: number = 1,
|
|
1184
|
+
limit: number = 50
|
|
1185
|
+
) {
|
|
1186
|
+
return this.employmentRepo.find({
|
|
1187
|
+
where: { department, isActive: true },
|
|
1188
|
+
relations: ['user'],
|
|
1189
|
+
skip: (page - 1) * limit,
|
|
1190
|
+
take: limit,
|
|
1191
|
+
order: { createdAt: 'DESC' }
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Add count for pagination UI
|
|
1196
|
+
async getEmployeeCount(department: string) {
|
|
1197
|
+
return this.employmentRepo.count({
|
|
1198
|
+
where: { department, isActive: true }
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
### 10. Monitor Query Performance
|
|
1204
|
+
|
|
1205
|
+
```typescript
|
|
1206
|
+
// Enable query logging in development
|
|
1207
|
+
TypeOrmModule.forRoot({
|
|
1208
|
+
// ...
|
|
1209
|
+
logging: ['query', 'error', 'warn'],
|
|
1210
|
+
maxQueryExecutionTime: 1000, // Warn if query takes > 1s
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
// Log slow queries in your service
|
|
1214
|
+
const startTime = Date.now();
|
|
1215
|
+
const result = await this.repo.find({ ... });
|
|
1216
|
+
const duration = Date.now() - startTime;
|
|
1217
|
+
|
|
1218
|
+
if (duration > 100) {
|
|
1219
|
+
this.logger.warn(`Slow query detected: ${duration}ms`);
|
|
1220
|
+
}
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
---
|
|
1224
|
+
|
|
1225
|
+
## 🔄 Migration from JSONB
|
|
1226
|
+
|
|
1227
|
+
If you have existing data in JSONB that needs to be queryable:
|
|
1228
|
+
|
|
1229
|
+
### Step 1: Create New Entity
|
|
1230
|
+
|
|
1231
|
+
```typescript
|
|
1232
|
+
@Entity('user_employment')
|
|
1233
|
+
@Index(['userId'])
|
|
1234
|
+
@Index(['department'])
|
|
1235
|
+
export class UserEmployment {
|
|
1236
|
+
@PrimaryGeneratedColumn('uuid')
|
|
1237
|
+
id: string;
|
|
1238
|
+
|
|
1239
|
+
@Column()
|
|
1240
|
+
userId: string;
|
|
1241
|
+
|
|
1242
|
+
@Column()
|
|
1243
|
+
department: string;
|
|
1244
|
+
|
|
1245
|
+
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
|
1246
|
+
salary: number;
|
|
1247
|
+
}
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
### Step 2: Create Migration
|
|
1251
|
+
|
|
1252
|
+
```bash
|
|
1253
|
+
npm run migration:generate --name=MigrateEmploymentData
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### Step 3: Write Migration Logic
|
|
1257
|
+
|
|
1258
|
+
```typescript
|
|
1259
|
+
export class MigrateEmploymentData1234567890 implements MigrationInterface {
|
|
1260
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
1261
|
+
// 1. Create table with indexes
|
|
1262
|
+
await queryRunner.query(`
|
|
1263
|
+
CREATE TABLE user_employment (
|
|
1264
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
1265
|
+
"userId" UUID NOT NULL,
|
|
1266
|
+
department VARCHAR(100),
|
|
1267
|
+
salary DECIMAL(10,2),
|
|
1268
|
+
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
CREATE INDEX idx_employment_user ON user_employment("userId");
|
|
1272
|
+
CREATE INDEX idx_employment_dept ON user_employment(department);
|
|
1273
|
+
`);
|
|
1274
|
+
|
|
1275
|
+
// 2. Migrate data
|
|
1276
|
+
await queryRunner.query(`
|
|
1277
|
+
INSERT INTO user_employment ("userId", department, salary)
|
|
1278
|
+
SELECT
|
|
1279
|
+
id,
|
|
1280
|
+
metadata->>'department',
|
|
1281
|
+
(metadata->>'salary')::DECIMAL
|
|
1282
|
+
FROM "user"
|
|
1283
|
+
WHERE metadata ? 'department';
|
|
1284
|
+
`);
|
|
1285
|
+
|
|
1286
|
+
// 3. Clean up old data
|
|
1287
|
+
await queryRunner.query(`
|
|
1288
|
+
UPDATE "user"
|
|
1289
|
+
SET metadata = metadata - 'department' - 'salary'
|
|
1290
|
+
WHERE metadata ? 'department';
|
|
1291
|
+
`);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
1295
|
+
// Migrate back if needed
|
|
1296
|
+
await queryRunner.query(`
|
|
1297
|
+
UPDATE "user"
|
|
1298
|
+
SET metadata = jsonb_set(
|
|
1299
|
+
jsonb_set(
|
|
1300
|
+
COALESCE(metadata, '{}'::jsonb),
|
|
1301
|
+
'{department}',
|
|
1302
|
+
to_jsonb(emp.department)
|
|
1303
|
+
),
|
|
1304
|
+
'{salary}',
|
|
1305
|
+
to_jsonb(emp.salary)
|
|
1306
|
+
)
|
|
1307
|
+
FROM user_employment emp
|
|
1308
|
+
WHERE "user".id = emp."userId";
|
|
1309
|
+
`);
|
|
1310
|
+
|
|
1311
|
+
await queryRunner.query(`DROP TABLE IF EXISTS user_employment;`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
### Step 4: Run Migration
|
|
1317
|
+
|
|
1318
|
+
```bash
|
|
1319
|
+
npm run migration:run
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
---
|
|
1323
|
+
|
|
1324
|
+
## 📚 Additional Resources
|
|
1325
|
+
|
|
1326
|
+
### Official Documentation
|
|
1327
|
+
- [NestJS Documentation](https://docs.nestjs.com)
|
|
1328
|
+
- [TypeORM Documentation](https://typeorm.io)
|
|
1329
|
+
- [PostgreSQL Performance Tips](https://wiki.postgresql.org/wiki/Performance_Optimization)
|
|
1330
|
+
|
|
1331
|
+
### Related Modules
|
|
1332
|
+
- `@venturialstd/core` - Core shared functionality
|
|
1333
|
+
- `@venturialstd/organization` - Organization management module
|
|
1334
|
+
- `@venturialstd/auth` - Authentication module
|
|
1335
|
+
|
|
1336
|
+
### Performance References
|
|
1337
|
+
- [PostgreSQL Index Types](https://www.postgresql.org/docs/current/indexes-types.html)
|
|
1338
|
+
- [JSONB Performance](https://www.postgresql.org/docs/current/datatype-json.html)
|
|
1339
|
+
- [Query Optimization](https://www.postgresql.org/docs/current/performance-tips.html)
|
|
1340
|
+
|
|
1341
|
+
---
|
|
1342
|
+
|
|
1343
|
+
## �📄 License
|
|
1344
|
+
|
|
1345
|
+
This module is part of the Venturial Standard Library.
|
|
1346
|
+
|
|
1347
|
+
---
|
|
1348
|
+
|
|
1349
|
+
## 🤝 Contributing
|
|
1350
|
+
|
|
1351
|
+
For contribution guidelines, please refer to the main repository's CONTRIBUTING.md.
|
|
1352
|
+
|
|
1353
|
+
---
|
|
1354
|
+
|
|
1355
|
+
## 📞 Support
|
|
1356
|
+
|
|
1357
|
+
For issues and questions, please open an issue in the main repository.
|