@flusys/nestjs-event-manager 3.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 +1328 -0
- package/adapters/event-manager.adapter.d.ts +12 -0
- package/adapters/index.d.ts +1 -0
- package/cjs/adapters/event-manager.adapter.js +71 -0
- package/cjs/adapters/index.js +18 -0
- package/cjs/config/event-manager.constants.js +13 -0
- package/cjs/config/index.js +18 -0
- package/cjs/controllers/event-participant.controller.js +271 -0
- package/cjs/controllers/event.controller.js +229 -0
- package/cjs/controllers/index.js +19 -0
- package/cjs/docs/event-manager-swagger.config.js +135 -0
- package/cjs/docs/index.js +24 -0
- package/cjs/dtos/calendar-query.dto.js +186 -0
- package/cjs/dtos/event-participant.dto.js +226 -0
- package/cjs/dtos/event.dto.js +291 -0
- package/cjs/dtos/index.js +20 -0
- package/cjs/entities/event-participant.entity.js +96 -0
- package/cjs/entities/event-with-company.entity.js +56 -0
- package/cjs/entities/event.entity.js +143 -0
- package/cjs/entities/index.js +55 -0
- package/cjs/index.js +27 -0
- package/cjs/interfaces/event-manager-module.interface.js +4 -0
- package/cjs/interfaces/event.interface.js +4 -0
- package/cjs/interfaces/index.js +19 -0
- package/cjs/modules/event-manager.module.js +133 -0
- package/cjs/modules/index.js +18 -0
- package/cjs/services/event-manager-config.service.js +76 -0
- package/cjs/services/event-manager-datasource.provider.js +181 -0
- package/cjs/services/event-manager-helper.service.js +253 -0
- package/cjs/services/event-participant.service.js +164 -0
- package/cjs/services/event.service.js +421 -0
- package/cjs/services/index.js +22 -0
- package/config/event-manager.constants.d.ts +1 -0
- package/config/index.d.ts +1 -0
- package/controllers/event-participant.controller.d.ts +31 -0
- package/controllers/event.controller.d.ts +24 -0
- package/controllers/index.d.ts +2 -0
- package/docs/event-manager-swagger.config.d.ts +3 -0
- package/docs/index.d.ts +2 -0
- package/dtos/calendar-query.dto.d.ts +23 -0
- package/dtos/event-participant.dto.d.ts +31 -0
- package/dtos/event.dto.d.ts +37 -0
- package/dtos/index.d.ts +3 -0
- package/entities/event-participant.entity.d.ts +8 -0
- package/entities/event-with-company.entity.d.ts +4 -0
- package/entities/event.entity.d.ts +14 -0
- package/entities/index.d.ts +11 -0
- package/fesm/adapters/event-manager.adapter.js +61 -0
- package/fesm/adapters/index.js +1 -0
- package/fesm/config/event-manager.constants.js +3 -0
- package/fesm/config/index.js +1 -0
- package/fesm/controllers/event-participant.controller.js +261 -0
- package/fesm/controllers/event.controller.js +219 -0
- package/fesm/controllers/index.js +2 -0
- package/fesm/docs/event-manager-swagger.config.js +128 -0
- package/fesm/docs/index.js +4 -0
- package/fesm/dtos/calendar-query.dto.js +168 -0
- package/fesm/dtos/event-participant.dto.js +199 -0
- package/fesm/dtos/event.dto.js +270 -0
- package/fesm/dtos/index.js +3 -0
- package/fesm/entities/event-participant.entity.js +86 -0
- package/fesm/entities/event-with-company.entity.js +46 -0
- package/fesm/entities/event.entity.js +133 -0
- package/fesm/entities/index.js +24 -0
- package/fesm/index.js +10 -0
- package/fesm/interfaces/event-manager-module.interface.js +3 -0
- package/fesm/interfaces/event.interface.js +1 -0
- package/fesm/interfaces/index.js +2 -0
- package/fesm/modules/event-manager.module.js +123 -0
- package/fesm/modules/index.js +1 -0
- package/fesm/services/event-manager-config.service.js +66 -0
- package/fesm/services/event-manager-datasource.provider.js +130 -0
- package/fesm/services/event-manager-helper.service.js +243 -0
- package/fesm/services/event-participant.service.js +154 -0
- package/fesm/services/event.service.js +411 -0
- package/fesm/services/index.js +5 -0
- package/index.d.ts +9 -0
- package/interfaces/event-manager-module.interface.d.ts +20 -0
- package/interfaces/event.interface.d.ts +28 -0
- package/interfaces/index.d.ts +2 -0
- package/modules/event-manager.module.d.ts +8 -0
- package/modules/index.d.ts +1 -0
- package/package.json +89 -0
- package/services/event-manager-config.service.d.ts +13 -0
- package/services/event-manager-datasource.provider.d.ts +23 -0
- package/services/event-manager-helper.service.d.ts +27 -0
- package/services/event-participant.service.d.ts +27 -0
- package/services/event.service.d.ts +49 -0
- package/services/index.d.ts +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,1328 @@
|
|
|
1
|
+
# Event Manager Package Guide
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flusys/nestjs-event-manager`
|
|
4
|
+
> **Version:** 3.0.1
|
|
5
|
+
> **Type:** Calendar event management with recurrence support and multi-tenant capabilities
|
|
6
|
+
|
|
7
|
+
This comprehensive guide covers the event manager package - calendar-based event scheduling with participants and recurrence patterns.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
- [Constants](#constants)
|
|
14
|
+
- [Package Architecture](#package-architecture)
|
|
15
|
+
- [Module Setup](#module-setup)
|
|
16
|
+
- [Entities](#entities)
|
|
17
|
+
- [Services](#services)
|
|
18
|
+
- [Controllers](#controllers)
|
|
19
|
+
- [DTOs](#dtos)
|
|
20
|
+
- [Recurrence Handling](#recurrence-handling)
|
|
21
|
+
- [REST API Endpoints](#rest-api-endpoints)
|
|
22
|
+
- [DataSource Provider Pattern](#datasource-provider-pattern)
|
|
23
|
+
- [Multi-Tenant Support](#multi-tenant-support)
|
|
24
|
+
- [Swagger Configuration](#swagger-configuration)
|
|
25
|
+
- [Event Manager Adapter](#event-manager-adapter)
|
|
26
|
+
- [Best Practices](#best-practices)
|
|
27
|
+
- [API Reference](#api-reference)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Overview
|
|
32
|
+
|
|
33
|
+
`@flusys/nestjs-event-manager` provides a comprehensive event management system:
|
|
34
|
+
|
|
35
|
+
- **Event CRUD** - Create, read, update, delete events with rich metadata
|
|
36
|
+
- **Recurrence Support** - Daily, weekly, biweekly, and monthly patterns
|
|
37
|
+
- **Participant Management** - Track event participants with status
|
|
38
|
+
- **Calendar Queries** - Date range queries with recurrence expansion
|
|
39
|
+
- **Adapter Pattern** - Cross-module event creation without direct dependencies
|
|
40
|
+
- **Company Scoping** - Per-company event isolation
|
|
41
|
+
- **Multi-Tenant Support** - Separate databases per tenant
|
|
42
|
+
|
|
43
|
+
### Package Hierarchy
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
@flusys/nestjs-core <- Foundation
|
|
47
|
+
|
|
|
48
|
+
@flusys/nestjs-shared <- Shared utilities
|
|
49
|
+
|
|
|
50
|
+
@flusys/nestjs-event-manager <- Event management (THIS PACKAGE)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @flusys/nestjs-event-manager @flusys/nestjs-shared @flusys/nestjs-core
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Constants
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// Injection Tokens
|
|
67
|
+
export const EVENT_MANAGER_MODULE_OPTIONS = 'EVENT_MANAGER_MODULE_OPTIONS';
|
|
68
|
+
|
|
69
|
+
// Adapter Token (from @flusys/nestjs-shared)
|
|
70
|
+
export const EVENT_MANAGER_ADAPTER = 'EVENT_MANAGER_ADAPTER';
|
|
71
|
+
|
|
72
|
+
// Permissions (from @flusys/nestjs-shared)
|
|
73
|
+
export const EVENT_PERMISSIONS = {
|
|
74
|
+
CREATE: 'event.create',
|
|
75
|
+
READ: 'event.read',
|
|
76
|
+
UPDATE: 'event.update',
|
|
77
|
+
DELETE: 'event.delete',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const EVENT_PARTICIPANT_PERMISSIONS = {
|
|
81
|
+
CREATE: 'event_participant.create',
|
|
82
|
+
READ: 'event_participant.read',
|
|
83
|
+
UPDATE: 'event_participant.update',
|
|
84
|
+
DELETE: 'event_participant.delete',
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Package Architecture
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
nestjs-event-manager/
|
|
94
|
+
├── src/
|
|
95
|
+
│ ├── modules/
|
|
96
|
+
│ │ └── event-manager.module.ts # Main module with provider registration
|
|
97
|
+
│ │
|
|
98
|
+
│ ├── adapters/
|
|
99
|
+
│ │ ├── event-manager.adapter.ts # Adapter for cross-module usage
|
|
100
|
+
│ │ └── index.ts
|
|
101
|
+
│ │
|
|
102
|
+
│ ├── config/
|
|
103
|
+
│ │ ├── event-manager.constants.ts # Module constants
|
|
104
|
+
│ │ └── index.ts
|
|
105
|
+
│ │
|
|
106
|
+
│ ├── services/
|
|
107
|
+
│ │ ├── event.service.ts # Event CRUD + calendar queries
|
|
108
|
+
│ │ ├── event-participant.service.ts # Participant CRUD
|
|
109
|
+
│ │ ├── event-manager-config.service.ts # Module configuration
|
|
110
|
+
│ │ ├── event-manager-datasource.provider.ts # Dynamic entity loading
|
|
111
|
+
│ │ ├── event-manager-helper.service.ts # Helper for adapter (singleton)
|
|
112
|
+
│ │ └── index.ts
|
|
113
|
+
│ │
|
|
114
|
+
│ ├── controllers/
|
|
115
|
+
│ │ ├── event.controller.ts # /event-manager/event/*
|
|
116
|
+
│ │ ├── event-participant.controller.ts # /event-manager/participant/*
|
|
117
|
+
│ │ └── index.ts
|
|
118
|
+
│ │
|
|
119
|
+
│ ├── entities/
|
|
120
|
+
│ │ ├── event.entity.ts # Event base
|
|
121
|
+
│ │ ├── event-with-company.entity.ts # Event with company
|
|
122
|
+
│ │ ├── event-participant.entity.ts # Participant entity
|
|
123
|
+
│ │ └── index.ts
|
|
124
|
+
│ │
|
|
125
|
+
│ ├── dtos/
|
|
126
|
+
│ │ ├── event.dto.ts # Event DTOs
|
|
127
|
+
│ │ ├── event-participant.dto.ts # Participant DTOs
|
|
128
|
+
│ │ ├── calendar-query.dto.ts # Calendar query DTO
|
|
129
|
+
│ │ └── index.ts
|
|
130
|
+
│ │
|
|
131
|
+
│ ├── interfaces/
|
|
132
|
+
│ │ ├── event.interface.ts # IEvent, IEventParticipant, ICalendarEvent
|
|
133
|
+
│ │ ├── event-manager-module.interface.ts # Module options
|
|
134
|
+
│ │ └── index.ts
|
|
135
|
+
│ │
|
|
136
|
+
│ ├── enums/
|
|
137
|
+
│ │ └── index.ts # Re-exports from @flusys/nestjs-shared
|
|
138
|
+
│ │
|
|
139
|
+
│ ├── docs/
|
|
140
|
+
│ │ └── index.ts # Swagger config, permissions
|
|
141
|
+
│ │
|
|
142
|
+
│ └── index.ts # Main export file
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Module Setup
|
|
148
|
+
|
|
149
|
+
### Basic Setup
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { Module } from '@nestjs/common';
|
|
153
|
+
import { EventManagerModule } from '@flusys/nestjs-event-manager';
|
|
154
|
+
|
|
155
|
+
@Module({
|
|
156
|
+
imports: [
|
|
157
|
+
EventManagerModule.forRoot({
|
|
158
|
+
global: true,
|
|
159
|
+
includeController: true,
|
|
160
|
+
bootstrapAppConfig: {
|
|
161
|
+
databaseMode: 'single',
|
|
162
|
+
enableCompanyFeature: false,
|
|
163
|
+
permissionMode: 'RBAC',
|
|
164
|
+
},
|
|
165
|
+
config: {
|
|
166
|
+
defaultDatabaseConfig: {
|
|
167
|
+
type: 'postgres',
|
|
168
|
+
host: 'localhost',
|
|
169
|
+
port: 5432,
|
|
170
|
+
username: 'postgres',
|
|
171
|
+
password: 'password',
|
|
172
|
+
database: 'myapp',
|
|
173
|
+
},
|
|
174
|
+
defaultColor: '#3B82F6',
|
|
175
|
+
maxRecurrenceOccurrences: 365,
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
],
|
|
179
|
+
})
|
|
180
|
+
export class AppModule {}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### With Company Feature
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
EventManagerModule.forRoot({
|
|
187
|
+
global: true,
|
|
188
|
+
includeController: true,
|
|
189
|
+
bootstrapAppConfig: {
|
|
190
|
+
databaseMode: 'single',
|
|
191
|
+
enableCompanyFeature: true,
|
|
192
|
+
permissionMode: 'FULL',
|
|
193
|
+
},
|
|
194
|
+
config: {
|
|
195
|
+
defaultDatabaseConfig: { /* ... */ },
|
|
196
|
+
defaultColor: '#3B82F6',
|
|
197
|
+
maxRecurrenceOccurrences: 365,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Async Configuration
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
EventManagerModule.forRootAsync({
|
|
206
|
+
global: true,
|
|
207
|
+
includeController: true,
|
|
208
|
+
bootstrapAppConfig: {
|
|
209
|
+
databaseMode: 'single',
|
|
210
|
+
enableCompanyFeature: true,
|
|
211
|
+
permissionMode: 'FULL',
|
|
212
|
+
},
|
|
213
|
+
imports: [ConfigModule],
|
|
214
|
+
useFactory: async (configService: ConfigService) => ({
|
|
215
|
+
defaultDatabaseConfig: configService.getDatabaseConfig(),
|
|
216
|
+
defaultColor: configService.get('EVENT_DEFAULT_COLOR', '#3B82F6'),
|
|
217
|
+
maxRecurrenceOccurrences: configService.get('MAX_RECURRENCE', 365),
|
|
218
|
+
}),
|
|
219
|
+
inject: [ConfigService],
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Configuration Options
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
interface IEventManagerModuleConfig extends IDataSourceServiceOptions {
|
|
227
|
+
/** Default event color (default: '#3B82F6') */
|
|
228
|
+
defaultColor?: string;
|
|
229
|
+
/** Maximum recurrence occurrences to generate (default: 365) */
|
|
230
|
+
maxRecurrenceOccurrences?: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface EventManagerModuleOptions extends IDynamicModuleConfig {
|
|
234
|
+
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
235
|
+
config?: IEventManagerModuleConfig;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Entities
|
|
242
|
+
|
|
243
|
+
### Enums (from @flusys/nestjs-shared)
|
|
244
|
+
|
|
245
|
+
The enums are defined in `@flusys/nestjs-shared/enums` and re-exported from this package for convenience:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// Import from shared (canonical source)
|
|
249
|
+
import { RecurrenceType, ParticipantStatus } from '@flusys/nestjs-shared/enums';
|
|
250
|
+
|
|
251
|
+
// Or import from event-manager (re-export)
|
|
252
|
+
import { RecurrenceType, ParticipantStatus } from '@flusys/nestjs-event-manager/enums';
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**RecurrenceType:**
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
export enum RecurrenceType {
|
|
259
|
+
NONE = 'none',
|
|
260
|
+
DAILY = 'daily',
|
|
261
|
+
WEEKLY = 'weekly',
|
|
262
|
+
BIWEEKLY = 'biweekly',
|
|
263
|
+
MONTHLY = 'monthly',
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**ParticipantStatus:**
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
export enum ParticipantStatus {
|
|
271
|
+
PENDING = 'pending',
|
|
272
|
+
ACCEPTED = 'accepted',
|
|
273
|
+
DECLINED = 'declined',
|
|
274
|
+
TENTATIVE = 'tentative',
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Entity Groups
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// Core entities (no company feature)
|
|
282
|
+
export const EventManagerCoreEntities = [Event, EventParticipant];
|
|
283
|
+
|
|
284
|
+
// Company-specific entities
|
|
285
|
+
export const EventManagerCompanyEntities = [EventWithCompany, EventParticipant];
|
|
286
|
+
|
|
287
|
+
// Helper function
|
|
288
|
+
export function getEventManagerEntitiesByConfig(enableCompanyFeature: boolean): any[] {
|
|
289
|
+
return enableCompanyFeature ? EventManagerCompanyEntities : EventManagerCoreEntities;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Base type aliases
|
|
293
|
+
export { Event as EventBase } from './event.entity';
|
|
294
|
+
export { EventParticipant as EventParticipantBase } from './event-participant.entity';
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Event Entity
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
@Entity({ name: 'event' })
|
|
301
|
+
@Index(['startDateTime', 'endDateTime'])
|
|
302
|
+
@Index(['isActive'])
|
|
303
|
+
export class Event extends Identity {
|
|
304
|
+
@Column({ type: 'varchar', length: 255 })
|
|
305
|
+
title!: string;
|
|
306
|
+
|
|
307
|
+
@Column({ type: 'text', nullable: true })
|
|
308
|
+
description!: string | null;
|
|
309
|
+
|
|
310
|
+
@Column({ type: 'varchar', length: 500, nullable: true })
|
|
311
|
+
meetingLink!: string | null;
|
|
312
|
+
|
|
313
|
+
@Column({ type: 'timestamp' })
|
|
314
|
+
@Index()
|
|
315
|
+
startDateTime!: Date;
|
|
316
|
+
|
|
317
|
+
@Column({ type: 'timestamp' })
|
|
318
|
+
endDateTime!: Date;
|
|
319
|
+
|
|
320
|
+
@Column({ type: 'boolean', default: false })
|
|
321
|
+
isAllDay!: boolean;
|
|
322
|
+
|
|
323
|
+
@Column({ type: 'varchar', length: 20, default: 'none' })
|
|
324
|
+
@Index()
|
|
325
|
+
recurrenceType!: string;
|
|
326
|
+
|
|
327
|
+
@Column({ type: 'timestamp', nullable: true })
|
|
328
|
+
recurrenceEndDate!: Date | null;
|
|
329
|
+
|
|
330
|
+
@Column({ type: 'varchar', length: 7, default: '#3B82F6' })
|
|
331
|
+
color!: string;
|
|
332
|
+
|
|
333
|
+
@Column({ type: 'boolean', default: true })
|
|
334
|
+
isActive!: boolean;
|
|
335
|
+
|
|
336
|
+
@Column({ type: 'simple-json', nullable: true })
|
|
337
|
+
metadata!: Record<string, unknown> | null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// With company feature
|
|
341
|
+
@Entity({ name: 'event' })
|
|
342
|
+
@Index(['companyId', 'isActive'])
|
|
343
|
+
export class EventWithCompany extends Event {
|
|
344
|
+
@Column({ type: 'uuid', nullable: true })
|
|
345
|
+
companyId!: string | null;
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### EventParticipant Entity
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
@Entity({ name: 'event_participant' })
|
|
353
|
+
@Index(['eventId'])
|
|
354
|
+
@Index(['userId'])
|
|
355
|
+
@Index(['eventId', 'userId'], { unique: true })
|
|
356
|
+
export class EventParticipant extends Identity {
|
|
357
|
+
@Column({ type: 'uuid' })
|
|
358
|
+
eventId!: string;
|
|
359
|
+
|
|
360
|
+
@Column({ type: 'uuid' })
|
|
361
|
+
userId!: string;
|
|
362
|
+
|
|
363
|
+
@Column({ type: 'varchar', length: 50, default: 'pending' })
|
|
364
|
+
status!: string;
|
|
365
|
+
|
|
366
|
+
@Column({ type: 'boolean', default: false })
|
|
367
|
+
isOrganizer!: boolean;
|
|
368
|
+
|
|
369
|
+
@Column({ type: 'simple-json', nullable: true })
|
|
370
|
+
metadata!: Record<string, unknown> | null;
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Note:** EventParticipant does NOT have a company variant - company context is inherited through the Event relationship.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Services
|
|
379
|
+
|
|
380
|
+
### EventService
|
|
381
|
+
|
|
382
|
+
Handles event CRUD and calendar queries.
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
386
|
+
export class EventService extends RequestScopedApiService<
|
|
387
|
+
CreateEventDto,
|
|
388
|
+
UpdateEventDto,
|
|
389
|
+
IEvent,
|
|
390
|
+
EventBase,
|
|
391
|
+
Repository<EventBase>
|
|
392
|
+
> {
|
|
393
|
+
// Standard CRUD methods inherited
|
|
394
|
+
|
|
395
|
+
/** Get events for calendar date range with recurrence expansion */
|
|
396
|
+
async getEventsForCalendarRange(
|
|
397
|
+
dto: CalendarQueryDto,
|
|
398
|
+
user?: ILoggedUserInfo
|
|
399
|
+
): Promise<ICalendarEvent[]>;
|
|
400
|
+
|
|
401
|
+
/** Get event with participant list */
|
|
402
|
+
async getEventWithParticipants(
|
|
403
|
+
eventId: string,
|
|
404
|
+
user?: ILoggedUserInfo
|
|
405
|
+
): Promise<EventBase | null>;
|
|
406
|
+
|
|
407
|
+
/** Add participants to event */
|
|
408
|
+
async addParticipants(
|
|
409
|
+
dto: BulkAddParticipantsDto,
|
|
410
|
+
user?: ILoggedUserInfo
|
|
411
|
+
): Promise<EventParticipantBase[]>;
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Key Features:**
|
|
416
|
+
- Dynamic entity resolution (Event vs EventWithCompany)
|
|
417
|
+
- Automatic participant creation on event insert
|
|
418
|
+
- Recurrence expansion for calendar queries
|
|
419
|
+
- Company filtering when enabled
|
|
420
|
+
|
|
421
|
+
### EventParticipantService
|
|
422
|
+
|
|
423
|
+
Handles participant CRUD.
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
427
|
+
export class EventParticipantService extends RequestScopedApiService<
|
|
428
|
+
CreateEventParticipantDto,
|
|
429
|
+
UpdateEventParticipantDto,
|
|
430
|
+
IEventParticipant,
|
|
431
|
+
EventParticipantBase,
|
|
432
|
+
Repository<EventParticipantBase>
|
|
433
|
+
> {
|
|
434
|
+
// Standard CRUD methods inherited
|
|
435
|
+
|
|
436
|
+
/** Update participant status */
|
|
437
|
+
async updateStatus(
|
|
438
|
+
dto: UpdateParticipantStatusDto,
|
|
439
|
+
user?: ILoggedUserInfo
|
|
440
|
+
): Promise<EventParticipantBase>;
|
|
441
|
+
|
|
442
|
+
/** Get participants for event */
|
|
443
|
+
async getParticipantsByEvent(eventId: string): Promise<EventParticipantBase[]>;
|
|
444
|
+
|
|
445
|
+
/** Get user's participations */
|
|
446
|
+
async getParticipantsByUser(userId: string): Promise<EventParticipantBase[]>;
|
|
447
|
+
|
|
448
|
+
/** Remove participant from event */
|
|
449
|
+
async removeParticipant(
|
|
450
|
+
eventId: string,
|
|
451
|
+
userId: string,
|
|
452
|
+
user?: ILoggedUserInfo
|
|
453
|
+
): Promise<void>;
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### EventManagerConfigService
|
|
458
|
+
|
|
459
|
+
Module configuration service.
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
@Injectable()
|
|
463
|
+
export class EventManagerConfigService implements IModuleConfigService {
|
|
464
|
+
/** Check if company feature is enabled */
|
|
465
|
+
isCompanyFeatureEnabled(): boolean;
|
|
466
|
+
|
|
467
|
+
/** Get database mode */
|
|
468
|
+
getDatabaseMode(): 'single' | 'multi-tenant';
|
|
469
|
+
|
|
470
|
+
/** Check if multi-tenant mode */
|
|
471
|
+
isMultiTenant(): boolean;
|
|
472
|
+
|
|
473
|
+
/** Get default event color */
|
|
474
|
+
getDefaultColor(): string;
|
|
475
|
+
|
|
476
|
+
/** Get max recurrence occurrences */
|
|
477
|
+
getMaxRecurrenceOccurrences(): number;
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### EventManagerDataSourceProvider
|
|
482
|
+
|
|
483
|
+
Dynamic entity loading with isolated cache.
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
487
|
+
export class EventManagerDataSourceProvider extends MultiTenantDataSourceService {
|
|
488
|
+
// Static cache isolated from other modules
|
|
489
|
+
protected static override readonly tenantConnections = new Map<string, DataSource>();
|
|
490
|
+
protected static override singleDataSource: DataSource | null = null;
|
|
491
|
+
|
|
492
|
+
/** Get entities based on company feature */
|
|
493
|
+
async getEventManagerEntities(enableCompanyFeature?: boolean): Promise<any[]>;
|
|
494
|
+
|
|
495
|
+
/** Get company feature for current tenant */
|
|
496
|
+
getEnableCompanyFeatureForCurrentTenant(): boolean;
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Controllers
|
|
503
|
+
|
|
504
|
+
### EventController
|
|
505
|
+
|
|
506
|
+
Base path: `/event-manager/event`
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
@Controller('event-manager/event')
|
|
510
|
+
@ApiTags('Event Manager - Events')
|
|
511
|
+
@ApiBearerAuth()
|
|
512
|
+
export class EventController extends createApiController<
|
|
513
|
+
CreateEventDto,
|
|
514
|
+
UpdateEventDto,
|
|
515
|
+
EventResponseDto,
|
|
516
|
+
IEvent,
|
|
517
|
+
EventService
|
|
518
|
+
>() {
|
|
519
|
+
// Inherited CRUD endpoints with permissions
|
|
520
|
+
|
|
521
|
+
@Post('calendar')
|
|
522
|
+
@UseGuards(JwtAuthGuard)
|
|
523
|
+
@RequirePermission(EVENT_PERMISSIONS.READ)
|
|
524
|
+
async getCalendarEvents(@Body() dto: CalendarQueryDto, @CurrentUser() user): Promise<ListResponseDto>;
|
|
525
|
+
|
|
526
|
+
@Post('get-with-participants')
|
|
527
|
+
@UseGuards(JwtAuthGuard)
|
|
528
|
+
@RequirePermission(EVENT_PERMISSIONS.READ)
|
|
529
|
+
async getWithParticipants(@Body() dto: { id: string }, @CurrentUser() user): Promise<SingleResponseDto>;
|
|
530
|
+
|
|
531
|
+
@Post('add-participants')
|
|
532
|
+
@UseGuards(JwtAuthGuard)
|
|
533
|
+
@RequirePermission(EVENT_PERMISSIONS.UPDATE)
|
|
534
|
+
async addParticipants(@Body() dto: BulkAddParticipantsDto, @CurrentUser() user): Promise<SingleResponseDto>;
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### EventParticipantController
|
|
539
|
+
|
|
540
|
+
Base path: `/event-manager/participant`
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
@Controller('event-manager/participant')
|
|
544
|
+
@ApiTags('Event Manager - Participants')
|
|
545
|
+
@ApiBearerAuth()
|
|
546
|
+
export class EventParticipantController extends createApiController<...>() {
|
|
547
|
+
// Inherited CRUD endpoints with permissions
|
|
548
|
+
|
|
549
|
+
@Post('update-status')
|
|
550
|
+
@UseGuards(JwtAuthGuard)
|
|
551
|
+
@RequirePermission(EVENT_PARTICIPANT_PERMISSIONS.UPDATE)
|
|
552
|
+
async updateStatus(@Body() dto: UpdateParticipantStatusDto, @CurrentUser() user): Promise<SingleResponseDto>;
|
|
553
|
+
|
|
554
|
+
@Post('get-by-event')
|
|
555
|
+
@UseGuards(JwtAuthGuard)
|
|
556
|
+
@RequirePermission(EVENT_PARTICIPANT_PERMISSIONS.READ)
|
|
557
|
+
async getByEvent(@Body() dto: { eventId: string }): Promise<ListResponseDto>;
|
|
558
|
+
|
|
559
|
+
@Post('get-by-user')
|
|
560
|
+
@UseGuards(JwtAuthGuard)
|
|
561
|
+
@RequirePermission(EVENT_PARTICIPANT_PERMISSIONS.READ)
|
|
562
|
+
async getByUser(@Body() dto: { userId: string }): Promise<ListResponseDto>;
|
|
563
|
+
|
|
564
|
+
@Post('remove')
|
|
565
|
+
@UseGuards(JwtAuthGuard)
|
|
566
|
+
@RequirePermission(EVENT_PARTICIPANT_PERMISSIONS.DELETE)
|
|
567
|
+
async remove(@Body() dto: { eventId: string; userId: string }, @CurrentUser() user): Promise<MessageResponseDto>;
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## DTOs
|
|
574
|
+
|
|
575
|
+
### CreateEventDto
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
export class CreateEventDto {
|
|
579
|
+
@IsNotEmpty()
|
|
580
|
+
@IsString()
|
|
581
|
+
@MaxLength(255)
|
|
582
|
+
title!: string;
|
|
583
|
+
|
|
584
|
+
@IsOptional()
|
|
585
|
+
@IsString()
|
|
586
|
+
description?: string;
|
|
587
|
+
|
|
588
|
+
@IsOptional()
|
|
589
|
+
@IsString()
|
|
590
|
+
@MaxLength(500)
|
|
591
|
+
meetingLink?: string;
|
|
592
|
+
|
|
593
|
+
@IsNotEmpty()
|
|
594
|
+
@IsDateString()
|
|
595
|
+
startDateTime!: Date;
|
|
596
|
+
|
|
597
|
+
@IsNotEmpty()
|
|
598
|
+
@IsDateString()
|
|
599
|
+
endDateTime!: Date;
|
|
600
|
+
|
|
601
|
+
@IsOptional()
|
|
602
|
+
@IsBoolean()
|
|
603
|
+
isAllDay?: boolean;
|
|
604
|
+
|
|
605
|
+
@IsOptional()
|
|
606
|
+
@IsEnum(RecurrenceType)
|
|
607
|
+
recurrenceType?: RecurrenceType;
|
|
608
|
+
|
|
609
|
+
@IsOptional()
|
|
610
|
+
@IsDateString()
|
|
611
|
+
recurrenceEndDate?: Date;
|
|
612
|
+
|
|
613
|
+
@IsOptional()
|
|
614
|
+
@IsString()
|
|
615
|
+
@MaxLength(7)
|
|
616
|
+
color?: string;
|
|
617
|
+
|
|
618
|
+
@IsOptional()
|
|
619
|
+
@IsBoolean()
|
|
620
|
+
isActive?: boolean;
|
|
621
|
+
|
|
622
|
+
@IsOptional()
|
|
623
|
+
metadata?: Record<string, unknown>;
|
|
624
|
+
|
|
625
|
+
@IsOptional()
|
|
626
|
+
@IsUUID('4', { each: true })
|
|
627
|
+
participantIds?: string[];
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### UpdateEventDto
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
export class UpdateEventDto extends PartialType(CreateEventDto) {
|
|
635
|
+
@IsUUID()
|
|
636
|
+
@IsNotEmpty()
|
|
637
|
+
id!: string;
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### CalendarQueryDto
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
export class CalendarQueryDto {
|
|
645
|
+
@IsNotEmpty()
|
|
646
|
+
@IsDateString()
|
|
647
|
+
startDate!: Date;
|
|
648
|
+
|
|
649
|
+
@IsNotEmpty()
|
|
650
|
+
@IsDateString()
|
|
651
|
+
endDate!: Date;
|
|
652
|
+
|
|
653
|
+
@IsOptional()
|
|
654
|
+
@IsBoolean()
|
|
655
|
+
includeParticipants?: boolean;
|
|
656
|
+
|
|
657
|
+
@IsOptional()
|
|
658
|
+
@IsUUID()
|
|
659
|
+
userId?: string;
|
|
660
|
+
|
|
661
|
+
@IsOptional()
|
|
662
|
+
@IsBoolean()
|
|
663
|
+
activeOnly?: boolean;
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### CreateEventParticipantDto
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
export class CreateEventParticipantDto {
|
|
671
|
+
@IsNotEmpty()
|
|
672
|
+
@IsUUID()
|
|
673
|
+
eventId!: string;
|
|
674
|
+
|
|
675
|
+
@IsNotEmpty()
|
|
676
|
+
@IsUUID()
|
|
677
|
+
userId!: string;
|
|
678
|
+
|
|
679
|
+
@IsOptional()
|
|
680
|
+
@IsEnum(ParticipantStatus)
|
|
681
|
+
status?: ParticipantStatus;
|
|
682
|
+
|
|
683
|
+
@IsOptional()
|
|
684
|
+
@IsBoolean()
|
|
685
|
+
isOrganizer?: boolean;
|
|
686
|
+
|
|
687
|
+
@IsOptional()
|
|
688
|
+
metadata?: Record<string, unknown>;
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### UpdateParticipantStatusDto
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
export class UpdateParticipantStatusDto {
|
|
696
|
+
@IsUUID()
|
|
697
|
+
@IsNotEmpty()
|
|
698
|
+
id!: string;
|
|
699
|
+
|
|
700
|
+
@IsNotEmpty()
|
|
701
|
+
@IsEnum(ParticipantStatus)
|
|
702
|
+
status!: ParticipantStatus;
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### BulkAddParticipantsDto
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
export class BulkAddParticipantsDto {
|
|
710
|
+
@IsNotEmpty()
|
|
711
|
+
@IsUUID()
|
|
712
|
+
eventId!: string;
|
|
713
|
+
|
|
714
|
+
@IsNotEmpty()
|
|
715
|
+
@IsUUID('4', { each: true })
|
|
716
|
+
userIds!: string[];
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
## Recurrence Handling
|
|
723
|
+
|
|
724
|
+
### How Recurrence Works
|
|
725
|
+
|
|
726
|
+
1. **Storage:** Events store recurrence pattern in `recurrenceType` and optional `recurrenceEndDate`
|
|
727
|
+
2. **Metadata:** Weekly recurrence uses `metadata.recurrenceDays` array (0=Sunday, 6=Saturday)
|
|
728
|
+
3. **Expansion:** Calendar queries expand recurring events into individual instances
|
|
729
|
+
4. **Instance Markers:** Expanded instances have `isRecurrenceInstance: true` and `originalEventId`
|
|
730
|
+
|
|
731
|
+
### Recurrence Expansion Logic
|
|
732
|
+
|
|
733
|
+
```typescript
|
|
734
|
+
private generateOccurrences(
|
|
735
|
+
event: EventBase,
|
|
736
|
+
rangeStart: Date,
|
|
737
|
+
rangeEnd: Date
|
|
738
|
+
): ICalendarEvent[] {
|
|
739
|
+
const occurrences: ICalendarEvent[] = [];
|
|
740
|
+
const recurrenceEnd = event.recurrenceEndDate || rangeEnd;
|
|
741
|
+
const maxOccurrences = this.eventConfig.getMaxRecurrenceOccurrences();
|
|
742
|
+
|
|
743
|
+
let currentDate = new Date(event.startDateTime);
|
|
744
|
+
|
|
745
|
+
while (currentDate <= recurrenceEnd && occurrences.length < maxOccurrences) {
|
|
746
|
+
if (currentDate >= rangeStart && currentDate <= rangeEnd) {
|
|
747
|
+
occurrences.push(this.createOccurrence(event, currentDate));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Advance based on recurrence type
|
|
751
|
+
switch (event.recurrenceType) {
|
|
752
|
+
case RecurrenceType.DAILY:
|
|
753
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
754
|
+
break;
|
|
755
|
+
case RecurrenceType.WEEKLY:
|
|
756
|
+
currentDate = this.getNextWeeklyOccurrence(event, currentDate);
|
|
757
|
+
break;
|
|
758
|
+
case RecurrenceType.BIWEEKLY:
|
|
759
|
+
currentDate.setDate(currentDate.getDate() + 14);
|
|
760
|
+
break;
|
|
761
|
+
case RecurrenceType.MONTHLY:
|
|
762
|
+
currentDate.setMonth(currentDate.getMonth() + 1);
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return occurrences;
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### Weekly Recurrence with Specific Days
|
|
772
|
+
|
|
773
|
+
```typescript
|
|
774
|
+
// Event with recurrence on Mon, Wed, Fri
|
|
775
|
+
const event = await eventService.insert({
|
|
776
|
+
title: 'Team Standup',
|
|
777
|
+
startDateTime: new Date('2026-03-02T09:00:00'),
|
|
778
|
+
endDateTime: new Date('2026-03-02T09:30:00'),
|
|
779
|
+
recurrenceType: RecurrenceType.WEEKLY,
|
|
780
|
+
recurrenceEndDate: new Date('2026-06-30'),
|
|
781
|
+
metadata: {
|
|
782
|
+
recurrenceDays: [1, 3, 5], // Monday, Wednesday, Friday
|
|
783
|
+
},
|
|
784
|
+
}, user);
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## REST API Endpoints
|
|
790
|
+
|
|
791
|
+
All event manager endpoints are prefixed with `/event-manager`.
|
|
792
|
+
|
|
793
|
+
### Event Endpoints
|
|
794
|
+
|
|
795
|
+
| Endpoint | Method | Description | Permission |
|
|
796
|
+
|----------|--------|-------------|------------|
|
|
797
|
+
| `/event-manager/event/insert` | POST | Create event | `event.create` |
|
|
798
|
+
| `/event-manager/event/insert-many` | POST | Bulk create | `event.create` |
|
|
799
|
+
| `/event-manager/event/get/:id` | POST | Get by ID | `event.read` |
|
|
800
|
+
| `/event-manager/event/get-all` | POST | List with pagination | `event.read` |
|
|
801
|
+
| `/event-manager/event/update` | POST | Update event | `event.update` |
|
|
802
|
+
| `/event-manager/event/update-many` | POST | Bulk update | `event.update` |
|
|
803
|
+
| `/event-manager/event/delete` | POST | Soft delete | `event.delete` |
|
|
804
|
+
| `/event-manager/event/calendar` | POST | Calendar query with expansion | `event.read` |
|
|
805
|
+
| `/event-manager/event/get-with-participants` | POST | Get with participant list | `event.read` |
|
|
806
|
+
| `/event-manager/event/add-participants` | POST | Add participants | `event.update` |
|
|
807
|
+
|
|
808
|
+
### Participant Endpoints
|
|
809
|
+
|
|
810
|
+
| Endpoint | Method | Description | Permission |
|
|
811
|
+
|----------|--------|-------------|------------|
|
|
812
|
+
| `/event-manager/participant/insert` | POST | Create participant | `event_participant.create` |
|
|
813
|
+
| `/event-manager/participant/insert-many` | POST | Bulk create | `event_participant.create` |
|
|
814
|
+
| `/event-manager/participant/get/:id` | POST | Get by ID | `event_participant.read` |
|
|
815
|
+
| `/event-manager/participant/get-all` | POST | List with pagination | `event_participant.read` |
|
|
816
|
+
| `/event-manager/participant/update` | POST | Update participant | `event_participant.update` |
|
|
817
|
+
| `/event-manager/participant/update-many` | POST | Bulk update | `event_participant.update` |
|
|
818
|
+
| `/event-manager/participant/delete` | POST | Soft delete | `event_participant.delete` |
|
|
819
|
+
| `/event-manager/participant/update-status` | POST | Update status | `event_participant.update` |
|
|
820
|
+
| `/event-manager/participant/get-by-event` | POST | Get by event | `event_participant.read` |
|
|
821
|
+
| `/event-manager/participant/get-by-user` | POST | Get by user | `event_participant.read` |
|
|
822
|
+
| `/event-manager/participant/remove` | POST | Remove from event | `event_participant.delete` |
|
|
823
|
+
|
|
824
|
+
### Request/Response Examples
|
|
825
|
+
|
|
826
|
+
**Create Event:**
|
|
827
|
+
|
|
828
|
+
```bash
|
|
829
|
+
curl -X POST http://localhost:2002/event-manager/event/insert \
|
|
830
|
+
-H "Authorization: Bearer <token>" \
|
|
831
|
+
-H "Content-Type: application/json" \
|
|
832
|
+
-d '{
|
|
833
|
+
"title": "Team Meeting",
|
|
834
|
+
"description": "Weekly sync",
|
|
835
|
+
"startDateTime": "2026-03-01T10:00:00Z",
|
|
836
|
+
"endDateTime": "2026-03-01T11:00:00Z",
|
|
837
|
+
"recurrenceType": "weekly",
|
|
838
|
+
"recurrenceEndDate": "2026-06-01T00:00:00Z",
|
|
839
|
+
"metadata": { "recurrenceDays": [1, 3, 5] },
|
|
840
|
+
"participantIds": ["user-id-1", "user-id-2"],
|
|
841
|
+
"color": "#3B82F6"
|
|
842
|
+
}'
|
|
843
|
+
|
|
844
|
+
# Response:
|
|
845
|
+
{
|
|
846
|
+
"success": true,
|
|
847
|
+
"message": "Event created successfully",
|
|
848
|
+
"data": {
|
|
849
|
+
"id": "event-uuid",
|
|
850
|
+
"title": "Team Meeting",
|
|
851
|
+
"startDateTime": "2026-03-01T10:00:00.000Z",
|
|
852
|
+
"recurrenceType": "weekly",
|
|
853
|
+
...
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
**Calendar Query:**
|
|
859
|
+
|
|
860
|
+
```bash
|
|
861
|
+
curl -X POST http://localhost:2002/event-manager/event/calendar \
|
|
862
|
+
-H "Authorization: Bearer <token>" \
|
|
863
|
+
-H "Content-Type: application/json" \
|
|
864
|
+
-d '{
|
|
865
|
+
"startDate": "2026-03-01",
|
|
866
|
+
"endDate": "2026-03-31",
|
|
867
|
+
"includeParticipants": true,
|
|
868
|
+
"activeOnly": true
|
|
869
|
+
}'
|
|
870
|
+
|
|
871
|
+
# Response includes expanded recurring events
|
|
872
|
+
{
|
|
873
|
+
"success": true,
|
|
874
|
+
"data": [
|
|
875
|
+
{
|
|
876
|
+
"id": "event-uuid",
|
|
877
|
+
"title": "Team Meeting",
|
|
878
|
+
"startDateTime": "2026-03-03T10:00:00.000Z",
|
|
879
|
+
"isRecurrenceInstance": true,
|
|
880
|
+
"originalEventId": "event-uuid",
|
|
881
|
+
"participants": [...]
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
"id": "event-uuid",
|
|
885
|
+
"title": "Team Meeting",
|
|
886
|
+
"startDateTime": "2026-03-05T10:00:00.000Z",
|
|
887
|
+
"isRecurrenceInstance": true,
|
|
888
|
+
"originalEventId": "event-uuid",
|
|
889
|
+
"participants": [...]
|
|
890
|
+
},
|
|
891
|
+
...
|
|
892
|
+
],
|
|
893
|
+
"meta": { "total": 15, "page": 1, "pageSize": 100, "count": 15 }
|
|
894
|
+
}
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## DataSource Provider Pattern
|
|
900
|
+
|
|
901
|
+
### EventManagerDataSourceProvider
|
|
902
|
+
|
|
903
|
+
The EventManagerDataSourceProvider follows the isolated cache pattern:
|
|
904
|
+
|
|
905
|
+
```typescript
|
|
906
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
907
|
+
export class EventManagerDataSourceProvider extends MultiTenantDataSourceService {
|
|
908
|
+
// Isolated static cache per module
|
|
909
|
+
protected static override readonly tenantConnections = new Map<string, DataSource>();
|
|
910
|
+
protected static override singleDataSource: DataSource | null = null;
|
|
911
|
+
protected static override tenantsRegistry: ITenant[] = [];
|
|
912
|
+
protected static override initialized = false;
|
|
913
|
+
protected static override connectionLocks = new Map<string, Promise<DataSource>>();
|
|
914
|
+
protected static override singleConnectionLock: Promise<DataSource> | null = null;
|
|
915
|
+
|
|
916
|
+
constructor(
|
|
917
|
+
@Inject(EventManagerConfigService) private readonly configService: EventManagerConfigService,
|
|
918
|
+
@Optional() @Inject(REQUEST) protected override readonly request?: Request,
|
|
919
|
+
) {
|
|
920
|
+
super(configService, request);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async getEventManagerEntities(enableCompanyFeature?: boolean): Promise<any[]> {
|
|
924
|
+
const featureEnabled = enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
|
|
925
|
+
return getEventManagerEntitiesByConfig(featureEnabled);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Override methods to use EventManagerDataSourceProvider's static cache
|
|
929
|
+
protected override async getSingleDataSource(): Promise<DataSource> {
|
|
930
|
+
// Uses EventManagerDataSourceProvider.singleDataSource
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
protected override async getOrCreateTenantConnection(tenant: ITenant): Promise<DataSource> {
|
|
934
|
+
// Uses EventManagerDataSourceProvider.tenantConnections
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
---
|
|
940
|
+
|
|
941
|
+
## Multi-Tenant Support
|
|
942
|
+
|
|
943
|
+
### Company Filtering
|
|
944
|
+
|
|
945
|
+
When company feature is enabled:
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
protected override async getExtraManipulateQuery(query, filterDto, user) {
|
|
949
|
+
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
950
|
+
|
|
951
|
+
applyCompanyFilter(query, {
|
|
952
|
+
isCompanyFeatureEnabled: this.eventConfig.isCompanyFeatureEnabled(),
|
|
953
|
+
entityAlias: 'event',
|
|
954
|
+
}, user);
|
|
955
|
+
|
|
956
|
+
return result;
|
|
957
|
+
}
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
### DTO to Entity Conversion
|
|
961
|
+
|
|
962
|
+
```typescript
|
|
963
|
+
protected override async convertSingleDtoToEntity(dto, user): Promise<Partial<EventBase>> {
|
|
964
|
+
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
965
|
+
|
|
966
|
+
if (this.eventConfig.isCompanyFeatureEnabled() && user?.companyId) {
|
|
967
|
+
(entity as Partial<EventWithCompany>).companyId = user.companyId;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return entity;
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
---
|
|
975
|
+
|
|
976
|
+
## Swagger Configuration
|
|
977
|
+
|
|
978
|
+
### Using eventManagerSwaggerConfig
|
|
979
|
+
|
|
980
|
+
```typescript
|
|
981
|
+
import { eventManagerSwaggerConfig } from '@flusys/nestjs-event-manager/docs';
|
|
982
|
+
|
|
983
|
+
// In your main.ts or app module
|
|
984
|
+
const swaggerOptions = eventManagerSwaggerConfig({
|
|
985
|
+
enableCompanyFeature: true,
|
|
986
|
+
databaseMode: 'single',
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// Returns:
|
|
990
|
+
// {
|
|
991
|
+
// title: 'Event Manager API',
|
|
992
|
+
// description: '... dynamic description based on config ...',
|
|
993
|
+
// version: '1.0',
|
|
994
|
+
// path: 'api/docs/event-manager',
|
|
995
|
+
// bearerAuth: true,
|
|
996
|
+
// }
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
## Event Manager Adapter
|
|
1002
|
+
|
|
1003
|
+
The Event Manager Adapter allows other modules to create events and manage participants without direct dependencies on the event manager module.
|
|
1004
|
+
|
|
1005
|
+
### Interface (from @flusys/nestjs-shared)
|
|
1006
|
+
|
|
1007
|
+
```typescript
|
|
1008
|
+
import { ParticipantStatus, RecurrenceType } from '@flusys/nestjs-shared/enums';
|
|
1009
|
+
|
|
1010
|
+
export interface IEventManagerAdapter {
|
|
1011
|
+
createEvent(options: CreateEventOptions): Promise<EventResult>;
|
|
1012
|
+
addParticipants(eventId: string, userIds: string[], companyId?: string): Promise<void>;
|
|
1013
|
+
removeParticipant(eventId: string, userId: string): Promise<void>;
|
|
1014
|
+
updateParticipantStatus(participantId: string, status: ParticipantStatus): Promise<void>;
|
|
1015
|
+
getEventsForUser?(userId: string, startDate: Date, endDate: Date, companyId?: string): Promise<EventResult[]>;
|
|
1016
|
+
getEventById?(eventId: string): Promise<EventResult | null>;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
export interface CreateEventOptions {
|
|
1020
|
+
title: string;
|
|
1021
|
+
description?: string;
|
|
1022
|
+
meetingLink?: string;
|
|
1023
|
+
startDateTime: Date;
|
|
1024
|
+
endDateTime: Date;
|
|
1025
|
+
isAllDay?: boolean;
|
|
1026
|
+
recurrenceType?: RecurrenceType;
|
|
1027
|
+
recurrenceEndDate?: Date;
|
|
1028
|
+
color?: string;
|
|
1029
|
+
metadata?: Record<string, unknown>;
|
|
1030
|
+
participantIds?: string[];
|
|
1031
|
+
organizerId?: string;
|
|
1032
|
+
companyId?: string;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export interface EventResult {
|
|
1036
|
+
id: string;
|
|
1037
|
+
title: string;
|
|
1038
|
+
description?: string | null;
|
|
1039
|
+
meetingLink?: string | null;
|
|
1040
|
+
startDateTime: Date;
|
|
1041
|
+
endDateTime: Date;
|
|
1042
|
+
isAllDay: boolean;
|
|
1043
|
+
recurrenceType: string;
|
|
1044
|
+
recurrenceEndDate?: Date | null;
|
|
1045
|
+
color: string;
|
|
1046
|
+
isActive: boolean;
|
|
1047
|
+
metadata?: Record<string, unknown> | null;
|
|
1048
|
+
companyId?: string | null;
|
|
1049
|
+
createdAt: Date;
|
|
1050
|
+
updatedAt: Date;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export const EVENT_MANAGER_ADAPTER = 'EVENT_MANAGER_ADAPTER';
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
### Using the Adapter in Other Modules
|
|
1057
|
+
|
|
1058
|
+
```typescript
|
|
1059
|
+
import { Injectable, Inject, Optional } from '@nestjs/common';
|
|
1060
|
+
import {
|
|
1061
|
+
EVENT_MANAGER_ADAPTER,
|
|
1062
|
+
IEventManagerAdapter,
|
|
1063
|
+
RecurrenceType,
|
|
1064
|
+
} from '@flusys/nestjs-shared';
|
|
1065
|
+
|
|
1066
|
+
@Injectable()
|
|
1067
|
+
export class MyService {
|
|
1068
|
+
constructor(
|
|
1069
|
+
@Optional() @Inject(EVENT_MANAGER_ADAPTER)
|
|
1070
|
+
private readonly eventAdapter?: IEventManagerAdapter,
|
|
1071
|
+
) {}
|
|
1072
|
+
|
|
1073
|
+
async createMeetingEvent(userId: string, companyId?: string) {
|
|
1074
|
+
if (!this.eventAdapter) {
|
|
1075
|
+
console.warn('Event manager not available');
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const event = await this.eventAdapter.createEvent({
|
|
1080
|
+
title: 'Team Meeting',
|
|
1081
|
+
description: 'Weekly sync meeting',
|
|
1082
|
+
startDateTime: new Date(),
|
|
1083
|
+
endDateTime: new Date(Date.now() + 3600000), // 1 hour later
|
|
1084
|
+
recurrenceType: RecurrenceType.WEEKLY,
|
|
1085
|
+
participantIds: [userId],
|
|
1086
|
+
organizerId: userId,
|
|
1087
|
+
companyId,
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
return event;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
async inviteToEvent(eventId: string, userIds: string[]) {
|
|
1094
|
+
if (this.eventAdapter) {
|
|
1095
|
+
await this.eventAdapter.addParticipants(eventId, userIds);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
### Module Registration Order
|
|
1102
|
+
|
|
1103
|
+
**Important:** The `EventManagerModule` must be imported BEFORE any module that injects the adapter:
|
|
1104
|
+
|
|
1105
|
+
```typescript
|
|
1106
|
+
@Module({
|
|
1107
|
+
imports: [
|
|
1108
|
+
// EventManagerModule FIRST
|
|
1109
|
+
EventManagerModule.forRoot({
|
|
1110
|
+
global: true,
|
|
1111
|
+
// ...config
|
|
1112
|
+
}),
|
|
1113
|
+
|
|
1114
|
+
// Modules that use the adapter AFTER
|
|
1115
|
+
MyFeatureModule,
|
|
1116
|
+
],
|
|
1117
|
+
})
|
|
1118
|
+
export class AppModule {}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### Adapter Methods
|
|
1122
|
+
|
|
1123
|
+
| Method | Description |
|
|
1124
|
+
|--------|-------------|
|
|
1125
|
+
| `createEvent(options)` | Create a new event with optional participants |
|
|
1126
|
+
| `addParticipants(eventId, userIds)` | Add users as participants to an event |
|
|
1127
|
+
| `removeParticipant(eventId, userId)` | Remove a user from an event |
|
|
1128
|
+
| `updateParticipantStatus(participantId, status)` | Update RSVP status |
|
|
1129
|
+
| `getEventsForUser(userId, startDate, endDate)` | Get user's events in date range |
|
|
1130
|
+
| `getEventById(eventId)` | Get a single event by ID |
|
|
1131
|
+
|
|
1132
|
+
### Helper Service
|
|
1133
|
+
|
|
1134
|
+
The adapter delegates to `EventManagerHelperService`, a singleton service that handles the actual business logic:
|
|
1135
|
+
|
|
1136
|
+
```typescript
|
|
1137
|
+
@Injectable()
|
|
1138
|
+
export class EventManagerHelperService {
|
|
1139
|
+
async createEvent(options: CreateEventOptions): Promise<EventResult>;
|
|
1140
|
+
async addParticipants(eventId: string, userIds: string[], companyId?: string): Promise<void>;
|
|
1141
|
+
async removeParticipant(eventId: string, userId: string): Promise<void>;
|
|
1142
|
+
async updateParticipantStatus(participantId: string, status: ParticipantStatus): Promise<void>;
|
|
1143
|
+
async getEventsForUser(userId: string, startDate: Date, endDate: Date, companyId?: string): Promise<EventResult[]>;
|
|
1144
|
+
async getEventById(eventId: string): Promise<EventResult | null>;
|
|
1145
|
+
}
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
---
|
|
1149
|
+
|
|
1150
|
+
## Best Practices
|
|
1151
|
+
|
|
1152
|
+
### 1. Use Proper Recurrence Patterns
|
|
1153
|
+
|
|
1154
|
+
```typescript
|
|
1155
|
+
// Daily standup
|
|
1156
|
+
await eventService.insert({
|
|
1157
|
+
title: 'Daily Standup',
|
|
1158
|
+
recurrenceType: RecurrenceType.DAILY,
|
|
1159
|
+
recurrenceEndDate: new Date('2026-12-31'),
|
|
1160
|
+
}, user);
|
|
1161
|
+
|
|
1162
|
+
// Weekly on specific days
|
|
1163
|
+
await eventService.insert({
|
|
1164
|
+
title: 'Team Sync',
|
|
1165
|
+
recurrenceType: RecurrenceType.WEEKLY,
|
|
1166
|
+
metadata: { recurrenceDays: [1, 4] }, // Monday, Thursday
|
|
1167
|
+
}, user);
|
|
1168
|
+
|
|
1169
|
+
// Monthly meeting
|
|
1170
|
+
await eventService.insert({
|
|
1171
|
+
title: 'Monthly Review',
|
|
1172
|
+
recurrenceType: RecurrenceType.MONTHLY,
|
|
1173
|
+
}, user);
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
### 2. Query Calendar Efficiently
|
|
1177
|
+
|
|
1178
|
+
```typescript
|
|
1179
|
+
// Only fetch what you need
|
|
1180
|
+
const events = await eventService.getEventsForCalendarRange({
|
|
1181
|
+
startDate: monthStart,
|
|
1182
|
+
endDate: monthEnd,
|
|
1183
|
+
includeParticipants: false, // Only include if needed
|
|
1184
|
+
activeOnly: true,
|
|
1185
|
+
}, user);
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
### 3. Handle Participant Status
|
|
1189
|
+
|
|
1190
|
+
```typescript
|
|
1191
|
+
// Update participant response
|
|
1192
|
+
await participantService.updateStatus({
|
|
1193
|
+
id: participantId,
|
|
1194
|
+
status: ParticipantStatus.ACCEPTED,
|
|
1195
|
+
}, user);
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
### 4. Use Consistent Colors
|
|
1199
|
+
|
|
1200
|
+
```typescript
|
|
1201
|
+
// Use the default color from config
|
|
1202
|
+
const defaultColor = eventConfig.getDefaultColor();
|
|
1203
|
+
|
|
1204
|
+
// Or use predefined colors
|
|
1205
|
+
const EVENT_COLORS = [
|
|
1206
|
+
'#3B82F6', // Blue
|
|
1207
|
+
'#EF4444', // Red
|
|
1208
|
+
'#10B981', // Green
|
|
1209
|
+
'#F59E0B', // Amber
|
|
1210
|
+
'#8B5CF6', // Purple
|
|
1211
|
+
];
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
---
|
|
1215
|
+
|
|
1216
|
+
## API Reference
|
|
1217
|
+
|
|
1218
|
+
### Main Exports
|
|
1219
|
+
|
|
1220
|
+
```typescript
|
|
1221
|
+
// Module
|
|
1222
|
+
import { EventManagerModule } from '@flusys/nestjs-event-manager';
|
|
1223
|
+
|
|
1224
|
+
// Adapter (for cross-module usage)
|
|
1225
|
+
import { EventManagerAdapter } from '@flusys/nestjs-event-manager/adapters';
|
|
1226
|
+
|
|
1227
|
+
// Adapter Interface & Token (from shared)
|
|
1228
|
+
import {
|
|
1229
|
+
EVENT_MANAGER_ADAPTER,
|
|
1230
|
+
IEventManagerAdapter,
|
|
1231
|
+
CreateEventOptions,
|
|
1232
|
+
EventResult,
|
|
1233
|
+
} from '@flusys/nestjs-shared/interfaces';
|
|
1234
|
+
|
|
1235
|
+
// Services
|
|
1236
|
+
import {
|
|
1237
|
+
EventService,
|
|
1238
|
+
EventParticipantService,
|
|
1239
|
+
EventManagerConfigService,
|
|
1240
|
+
EventManagerDataSourceProvider,
|
|
1241
|
+
EventManagerHelperService,
|
|
1242
|
+
} from '@flusys/nestjs-event-manager/services';
|
|
1243
|
+
|
|
1244
|
+
// Entities
|
|
1245
|
+
import {
|
|
1246
|
+
Event,
|
|
1247
|
+
EventBase,
|
|
1248
|
+
EventWithCompany,
|
|
1249
|
+
EventParticipant,
|
|
1250
|
+
EventParticipantBase,
|
|
1251
|
+
EventManagerCoreEntities,
|
|
1252
|
+
EventManagerCompanyEntities,
|
|
1253
|
+
getEventManagerEntitiesByConfig,
|
|
1254
|
+
} from '@flusys/nestjs-event-manager/entities';
|
|
1255
|
+
|
|
1256
|
+
// DTOs
|
|
1257
|
+
import {
|
|
1258
|
+
CreateEventDto,
|
|
1259
|
+
UpdateEventDto,
|
|
1260
|
+
EventResponseDto,
|
|
1261
|
+
CalendarEventResponseDto,
|
|
1262
|
+
CalendarQueryDto,
|
|
1263
|
+
CreateEventParticipantDto,
|
|
1264
|
+
UpdateEventParticipantDto,
|
|
1265
|
+
EventParticipantResponseDto,
|
|
1266
|
+
UpdateParticipantStatusDto,
|
|
1267
|
+
BulkAddParticipantsDto,
|
|
1268
|
+
} from '@flusys/nestjs-event-manager/dtos';
|
|
1269
|
+
|
|
1270
|
+
// Interfaces
|
|
1271
|
+
import {
|
|
1272
|
+
IEvent,
|
|
1273
|
+
IEventParticipant,
|
|
1274
|
+
ICalendarEvent,
|
|
1275
|
+
IEventManagerModuleConfig,
|
|
1276
|
+
EventManagerModuleOptions,
|
|
1277
|
+
EventManagerModuleAsyncOptions,
|
|
1278
|
+
EventManagerOptionsFactory,
|
|
1279
|
+
} from '@flusys/nestjs-event-manager/interfaces';
|
|
1280
|
+
|
|
1281
|
+
// Enums (canonical source)
|
|
1282
|
+
import {
|
|
1283
|
+
RecurrenceType,
|
|
1284
|
+
ParticipantStatus,
|
|
1285
|
+
} from '@flusys/nestjs-shared/enums';
|
|
1286
|
+
|
|
1287
|
+
// Or re-export from event-manager
|
|
1288
|
+
import {
|
|
1289
|
+
RecurrenceType,
|
|
1290
|
+
ParticipantStatus,
|
|
1291
|
+
} from '@flusys/nestjs-event-manager/enums';
|
|
1292
|
+
|
|
1293
|
+
// Controllers
|
|
1294
|
+
import {
|
|
1295
|
+
EventController,
|
|
1296
|
+
EventParticipantController,
|
|
1297
|
+
} from '@flusys/nestjs-event-manager/controllers';
|
|
1298
|
+
|
|
1299
|
+
// Config & Docs
|
|
1300
|
+
import { EVENT_MANAGER_MODULE_OPTIONS } from '@flusys/nestjs-event-manager/config';
|
|
1301
|
+
import {
|
|
1302
|
+
EVENT_PERMISSIONS,
|
|
1303
|
+
EVENT_PARTICIPANT_PERMISSIONS,
|
|
1304
|
+
eventManagerSwaggerConfig,
|
|
1305
|
+
} from '@flusys/nestjs-event-manager/docs';
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
---
|
|
1309
|
+
|
|
1310
|
+
## Summary
|
|
1311
|
+
|
|
1312
|
+
The `@flusys/nestjs-event-manager` package provides:
|
|
1313
|
+
|
|
1314
|
+
- **Event CRUD** - Full create, read, update, delete for events
|
|
1315
|
+
- **Recurrence Support** - Daily, weekly, biweekly, monthly patterns
|
|
1316
|
+
- **Calendar Queries** - Date range queries with automatic recurrence expansion
|
|
1317
|
+
- **Participant Management** - Track participants with status (pending, accepted, declined, tentative)
|
|
1318
|
+
- **Adapter Pattern** - Cross-module event creation via `IEventManagerAdapter`
|
|
1319
|
+
- **Company Scoping** - Per-company event isolation
|
|
1320
|
+
- **Multi-Tenant** - Separate databases per tenant
|
|
1321
|
+
- **POST-only RPC** - All endpoints use POST method
|
|
1322
|
+
- **Permission Guards** - All endpoints protected by permissions
|
|
1323
|
+
|
|
1324
|
+
---
|
|
1325
|
+
|
|
1326
|
+
**Last Updated:** 2026-02-26
|
|
1327
|
+
**Version:** 3.0.0
|
|
1328
|
+
**NestJS Version:** 11
|