@venturialstd/organization 0.0.2 โ†’ 0.0.3

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 CHANGED
@@ -1,910 +1,910 @@
1
- # @venturialstd/organization
2
-
3
- A comprehensive NestJS module for managing organizations with multi-tenant capabilities, role-based access control, and isolated per-organization settings.
4
-
5
- ## ๐Ÿ“‹ Table of Contents
6
-
7
- - [Features](#features)
8
- - [Installation](#installation)
9
- - [Quick Start](#quick-start)
10
- - [Core Concepts](#core-concepts)
11
- - [Architecture](#architecture)
12
- - [API Reference](#api-reference)
13
- - [Organization Settings System](#organization-settings-system)
14
- - [Test Server](#test-server)
15
- - [Migration Guide](#migration-guide)
16
- - [Examples](#examples)
17
- - [Best Practices](#best-practices)
18
-
19
- ---
20
-
21
- ## โœจ Features
22
-
23
- - ๐Ÿข **Organization Management**: Complete CRUD operations for organizations
24
- - ๐Ÿ‘ฅ **Member Management**: Add, invite, and manage organization members
25
- - ๐Ÿ” **Role-Based Access Control**: Owner, Admin, Member, and Viewer roles
26
- - โš™๏ธ **Isolated Settings System**: Per-organization configuration for modules
27
- - ๐Ÿ”’ **Complete Isolation**: No fallback between organizations
28
- - ๐Ÿ›ก๏ธ **Guards & Decorators**: Request-level access control
29
- - ๐Ÿ”„ **TypeORM Integration**: Full database support with migrations
30
- - ๐Ÿ“ฆ **Fully Typed**: Complete TypeScript support
31
- - ๐Ÿงช **Test Infrastructure**: Standalone test server
32
-
33
- ---
34
-
35
- ## ๐Ÿ“ฆ Installation
36
-
37
- \`\`\`bash
38
- npm install @venturialstd/organization
39
- \`\`\`
40
-
41
- ### Peer Dependencies
42
-
43
- \`\`\`json
44
- {
45
- "@nestjs/common": "^11.0.11",
46
- "@nestjs/core": "^11.0.5",
47
- "@nestjs/typeorm": "^10.0.0",
48
- "@venturialstd/core": "^1.0.16",
49
- "class-transformer": "^0.5.1",
50
- "class-validator": "^0.14.1",
51
- "typeorm": "^0.3.20"
52
- }
53
- \`\`\`
54
-
55
- ---
56
-
57
- ## ๐Ÿš€ Quick Start
58
-
59
- ### 1. Import the Module
60
-
61
- \`\`\`typescript
62
- import { Module } from '@nestjs/common';
63
- import { OrganizationModule } from '@venturialstd/organization';
64
-
65
- @Module({
66
- imports: [OrganizationModule],
67
- })
68
- export class AppModule {}
69
- \`\`\`
70
-
71
- ### 2. Run Migrations
72
-
73
- \`\`\`bash
74
- # Create and run migrations
75
- npm run migration:generate -- CreateOrganizations
76
- npm run migration:run
77
- \`\`\`
78
-
79
- ### 3. Use the Services
80
-
81
- \`\`\`typescript
82
- import { Injectable } from '@nestjs/common';
83
- import {
84
- OrganizationService,
85
- OrganizationUserService,
86
- ORGANIZATION_USER_ROLE
87
- } from '@venturialstd/organization';
88
-
89
- @Injectable()
90
- export class YourService {
91
- constructor(
92
- private readonly organizationService: OrganizationService,
93
- private readonly organizationUserService: OrganizationUserService,
94
- ) {}
95
-
96
- async createOrganization(userId: string) {
97
- return this.organizationService.createOrganization(
98
- 'My Organization',
99
- 'my-org',
100
- 'example.com',
101
- 'Description',
102
- userId
103
- );
104
- }
105
-
106
- async addMember(organizationId: string, userId: string) {
107
- return this.organizationUserService.addUserToOrganization(
108
- organizationId,
109
- userId,
110
- ORGANIZATION_USER_ROLE.MEMBER
111
- );
112
- }
113
- }
114
- \`\`\`
115
-
116
- ---
117
-
118
- ## ๐Ÿ’ก Core Concepts
119
-
120
- ### Organizations
121
-
122
- An organization represents a tenant in your multi-tenant application. Each organization:
123
- - Has a unique name and slug
124
- - Can have a custom domain
125
- - Has its own settings and configuration
126
- - Contains multiple members with different roles
127
- - Is completely isolated from other organizations
128
-
129
- ### Organization Members
130
-
131
- Users can belong to multiple organizations with different roles:
132
- - **Owner**: Full control, can delete organization
133
- - **Admin**: Can manage members and settings
134
- - **Member**: Standard access to organization resources
135
- - **Viewer**: Read-only access
136
-
137
- ### Organization Settings
138
-
139
- Each organization can have isolated settings for any module in your application:
140
- - **Complete Isolation**: No sharing between organizations
141
- - **No Fallback**: Each organization must configure its own settings
142
- - **Type-Safe**: Fully typed with TypeScript
143
- - **Module-Agnostic**: Works with any module (GitLab, Jira, Slack, etc.)
144
-
145
- ---
146
-
147
- ## ๐Ÿ—๏ธ Architecture
148
-
149
- ### Module Structure
150
-
151
- \`\`\`
152
- OrganizationModule
153
- โ”œโ”€โ”€ Entities
154
- โ”‚ โ”œโ”€โ”€ Organization # Organization entity
155
- โ”‚ โ”œโ”€โ”€ OrganizationUser # User-Organization relationship
156
- โ”‚ โ””โ”€โ”€ OrganizationSettings # Per-organization settings
157
- โ”œโ”€โ”€ Services
158
- โ”‚ โ”œโ”€โ”€ OrganizationService # CRUD operations
159
- โ”‚ โ”œโ”€โ”€ OrganizationUserService # Member management
160
- โ”‚ โ””โ”€โ”€ OrganizationSettingsService # Settings management
161
- โ”œโ”€โ”€ Guards
162
- โ”‚ โ”œโ”€โ”€ OrganizationGuard # Check user belongs to org
163
- โ”‚ โ””โ”€โ”€ OrganizationRoleGuard # Check user role
164
- โ””โ”€โ”€ Decorators
165
- โ”œโ”€โ”€ @OrganizationId() # Extract org ID from request
166
- โ”œโ”€โ”€ @UserId() # Extract user ID from request
167
- โ”œโ”€โ”€ @Roles() # Define required roles
168
- โ””โ”€โ”€ @OrganizationContext() # Extract full context
169
- \`\`\`
170
-
171
- ### Settings Isolation
172
-
173
- \`\`\`
174
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
175
- โ”‚ Application โ”‚
176
- โ”‚ โ”‚
177
- โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
178
- โ”‚ โ”‚ Organization A โ”‚ โ”‚
179
- โ”‚ โ”‚ โ€ข GitLab: gitlab-a.com + token-a โ”‚ โ”‚
180
- โ”‚ โ”‚ โ€ข Jira: jira-a.com + credentials-a โ”‚ โ”‚
181
- โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
182
- โ”‚ โ”‚
183
- โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
184
- โ”‚ โ”‚ Organization B โ”‚ โ”‚
185
- โ”‚ โ”‚ โ€ข GitLab: gitlab.com + token-b โ”‚ โ”‚
186
- โ”‚ โ”‚ โ€ข Jira: [Not configured] โ”‚ โ”‚
187
- โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
188
- โ”‚ โ”‚
189
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
190
- \`\`\`
191
-
192
- ---
193
-
194
- ## ๐Ÿ“š API Reference
195
-
196
- ### Services
197
-
198
- #### OrganizationService
199
-
200
- \`\`\`typescript
201
- class OrganizationService {
202
- // Create a new organization
203
- createOrganization(
204
- name: string,
205
- slug: string,
206
- domain: string,
207
- description: string,
208
- ownerId: string
209
- ): Promise<Organization>
210
-
211
- // Get organization by ID
212
- getOrganizationById(id: string): Promise<Organization>
213
-
214
- // Update organization
215
- updateOrganization(id: string, data: Partial<Organization>): Promise<Organization>
216
-
217
- // Delete organization
218
- deleteOrganization(id: string): Promise<void>
219
-
220
- // Get user's organizations
221
- getUserOrganizations(userId: string): Promise<Organization[]>
222
- }
223
- \`\`\`
224
-
225
- #### OrganizationUserService
226
-
227
- \`\`\`typescript
228
- class OrganizationUserService {
229
- // Add user to organization
230
- addUserToOrganization(
231
- organizationId: string,
232
- userId: string,
233
- role: ORGANIZATION_USER_ROLE
234
- ): Promise<OrganizationUser>
235
-
236
- // Remove user from organization
237
- removeUserFromOrganization(
238
- organizationId: string,
239
- userId: string
240
- ): Promise<void>
241
-
242
- // Update user role
243
- updateUserRole(
244
- organizationId: string,
245
- userId: string,
246
- role: ORGANIZATION_USER_ROLE
247
- ): Promise<OrganizationUser>
248
-
249
- // Get organization members
250
- getOrganizationMembers(organizationId: string): Promise<OrganizationUser[]>
251
-
252
- // Check user role
253
- getUserRole(
254
- organizationId: string,
255
- userId: string
256
- ): Promise<ORGANIZATION_USER_ROLE>
257
- }
258
- \`\`\`
259
-
260
- #### OrganizationSettingsService
261
-
262
- \`\`\`typescript
263
- class OrganizationSettingsService {
264
- // Get a single setting
265
- get(organizationId: string, key: string): Promise<string | undefined>
266
-
267
- // Get multiple settings
268
- getManySettings(
269
- organizationId: string,
270
- keys: string[]
271
- ): Promise<Record<string, string>>
272
-
273
- // Get all settings for organization
274
- getAllForOrganization(
275
- organizationId: string,
276
- module?: string
277
- ): Promise<OrganizationSettings[]>
278
-
279
- // Set or update a setting
280
- setOrganizationSetting(
281
- organizationId: string,
282
- dto: Partial<OrganizationSettings>
283
- ): Promise<OrganizationSettings>
284
-
285
- // Delete a setting
286
- deleteOrganizationSetting(
287
- organizationId: string,
288
- module: string,
289
- section: string,
290
- key: string
291
- ): Promise<void>
292
-
293
- // Reset all settings for a module
294
- resetModuleSettings(
295
- organizationId: string,
296
- module: string
297
- ): Promise<void>
298
-
299
- // Bulk update settings
300
- bulkUpdateSettings(
301
- organizationId: string,
302
- settings: Partial<OrganizationSettings>[]
303
- ): Promise<OrganizationSettings[]>
304
- }
305
- \`\`\`
306
-
307
- ### Entities
308
-
309
- #### Organization
310
-
311
- \`\`\`typescript
312
- class Organization {
313
- id: string;
314
- name: string;
315
- slug: string;
316
- domain: string;
317
- description: string;
318
- isActive: boolean;
319
- settings: Record<string, unknown>;
320
- ownerId: string;
321
- createdAt: Date;
322
- updatedAt: Date;
323
- }
324
- \`\`\`
325
-
326
- #### OrganizationUser
327
-
328
- \`\`\`typescript
329
- class OrganizationUser {
330
- id: string;
331
- organizationId: string;
332
- userId: string;
333
- role: ORGANIZATION_USER_ROLE;
334
- isActive: boolean;
335
- createdAt: Date;
336
- updatedAt: Date;
337
- }
338
- \`\`\`
339
-
340
- #### OrganizationSettings
341
-
342
- \`\`\`typescript
343
- class OrganizationSettings {
344
- id: string;
345
- organizationId: string;
346
- module: string; // e.g., 'GITLAB', 'JIRA'
347
- section: string; // e.g., 'CREDENTIALS', 'OAUTH'
348
- key: string; // e.g., 'TOKEN', 'API_KEY'
349
- value: string;
350
- type: SETTINGS_TYPE;
351
- label: string;
352
- description: string;
353
- options: object | null;
354
- sortOrder: number;
355
- createdAt: Date;
356
- updatedAt: Date;
357
- }
358
- \`\`\`
359
-
360
- ### Decorators
361
-
362
- \`\`\`typescript
363
- // Extract organization ID from request
364
- @Get()
365
- getData(@OrganizationId() organizationId: string) { }
366
-
367
- // Extract user ID from request
368
- @Get()
369
- getData(@UserId() userId: string) { }
370
-
371
- // Extract full context
372
- @Get()
373
- getData(@OrganizationContext() context: OrganizationContextType) { }
374
-
375
- // Define required roles
376
- @Roles(ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER)
377
- @UseGuards(OrganizationRoleGuard)
378
- adminEndpoint() { }
379
- \`\`\`
380
-
381
- ### Guards
382
-
383
- \`\`\`typescript
384
- // Check user belongs to organization
385
- @UseGuards(OrganizationGuard)
386
- @Get('organizations/:organizationId/data')
387
- getData() { }
388
-
389
- // Check user has required role
390
- @Roles(ORGANIZATION_USER_ROLE.ADMIN)
391
- @UseGuards(OrganizationRoleGuard)
392
- adminEndpoint() { }
393
- \`\`\`
394
-
395
- ### Constants
396
-
397
- \`\`\`typescript
398
- // User roles
399
- enum ORGANIZATION_USER_ROLE {
400
- OWNER = 'owner',
401
- ADMIN = 'admin',
402
- MEMBER = 'member',
403
- VIEWER = 'viewer',
404
- }
405
-
406
- // Settings types
407
- enum SETTINGS_TYPE {
408
- TEXT = 'text',
409
- TEXTAREA = 'textarea',
410
- NUMBER = 'number',
411
- BOOLEAN = 'boolean',
412
- SELECT = 'select',
413
- MULTISELECT = 'multiselect',
414
- JSON = 'json',
415
- DATE = 'date',
416
- SECRET = 'secret',
417
- }
418
- \`\`\`
419
-
420
- ---
421
-
422
- ## โš™๏ธ Organization Settings System
423
-
424
- ### Complete Isolation
425
-
426
- Organization settings are **completely isolated** with no fallback mechanism:
427
-
428
- \`\`\`typescript
429
- // Organization A
430
- const tokenA = await orgSettings.get('org-a-id', 'GITLAB:CREDENTIALS:TOKEN');
431
- // Returns: 'token-a'
432
-
433
- // Organization B (not configured)
434
- const tokenB = await orgSettings.get('org-b-id', 'GITLAB:CREDENTIALS:TOKEN');
435
- // Returns: undefined (NO FALLBACK)
436
- \`\`\`
437
-
438
- ### Settings Types
439
-
440
- Use \`SETTINGS_TYPE\` enum for type-safe settings:
441
-
442
- \`\`\`typescript
443
- import { SETTINGS_TYPE } from '@venturialstd/organization';
444
-
445
- const settings = [
446
- {
447
- module: 'GITLAB',
448
- section: 'CREDENTIALS',
449
- key: 'TOKEN',
450
- type: SETTINGS_TYPE.SECRET, // Hides value in UI
451
- // ...
452
- },
453
- {
454
- module: 'GITLAB',
455
- section: 'PREFERENCES',
456
- key: 'AUTO_MERGE',
457
- type: SETTINGS_TYPE.BOOLEAN, // true/false toggle
458
- // ...
459
- },
460
- ];
461
- \`\`\`
462
-
463
- ### Using Settings in Your Module
464
-
465
- #### 1. Define Settings for Your Module
466
-
467
- \`\`\`typescript
468
- // src/your-module/settings/your-module.settings.ts
469
- import { SETTINGS_TYPE } from '@venturialstd/organization';
470
-
471
- export const SETTINGS = [
472
- {
473
- scope: 'ORGANIZATION',
474
- module: 'YOUR_MODULE',
475
- section: 'CREDENTIALS',
476
- key: 'API_KEY',
477
- label: 'API Key',
478
- description: 'Your module API key',
479
- type: SETTINGS_TYPE.SECRET,
480
- value: '',
481
- options: {
482
- placeholder: 'Enter your API key',
483
- help: 'Get your API key from your module dashboard',
484
- },
485
- sortOrder: 10,
486
- },
487
- {
488
- scope: 'ORGANIZATION',
489
- module: 'YOUR_MODULE',
490
- section: 'CREDENTIALS',
491
- key: 'API_SECRET',
492
- label: 'API Secret',
493
- description: 'Your module API secret',
494
- type: SETTINGS_TYPE.SECRET,
495
- value: '',
496
- options: null,
497
- sortOrder: 11,
498
- },
499
- ];
500
- \`\`\`
501
-
502
- #### 2. Use Settings in Your Service
503
-
504
- \`\`\`typescript
505
- import { Injectable } from '@nestjs/common';
506
- import { OrganizationSettingsService } from '@venturialstd/organization';
507
-
508
- @Injectable()
509
- export class YourModuleService {
510
- constructor(
511
- private readonly orgSettings: OrganizationSettingsService
512
- ) {}
513
-
514
- async connect(organizationId: string) {
515
- // Get organization-specific credentials
516
- const apiKey = await this.orgSettings.get(
517
- organizationId,
518
- 'YOUR_MODULE:CREDENTIALS:API_KEY'
519
- );
520
-
521
- const apiSecret = await this.orgSettings.get(
522
- organizationId,
523
- 'YOUR_MODULE:CREDENTIALS:API_SECRET'
524
- );
525
-
526
- // Check if configured
527
- if (!apiKey || !apiSecret) {
528
- throw new Error('Module not configured for this organization');
529
- }
530
-
531
- // Use organization-specific credentials
532
- return this.client.connect({ apiKey, apiSecret });
533
- }
534
-
535
- async getMultipleSettings(organizationId: string) {
536
- // Get multiple settings at once
537
- const settings = await this.orgSettings.getManySettings(
538
- organizationId,
539
- [
540
- 'YOUR_MODULE:CREDENTIALS:API_KEY',
541
- 'YOUR_MODULE:CREDENTIALS:API_SECRET',
542
- 'YOUR_MODULE:PREFERENCES:TIMEOUT',
543
- ]
544
- );
545
-
546
- return settings;
547
- }
548
- }
549
- \`\`\`
550
-
551
- #### 3. Import OrganizationModule
552
-
553
- \`\`\`typescript
554
- import { Module } from '@nestjs/common';
555
- import { OrganizationModule } from '@venturialstd/organization';
556
-
557
- @Module({
558
- imports: [OrganizationModule],
559
- providers: [YourModuleService],
560
- })
561
- export class YourModule {}
562
- \`\`\`
563
-
564
- ### Database Schema
565
-
566
- \`\`\`sql
567
- CREATE TABLE organization_settings (
568
- id UUID PRIMARY KEY,
569
- organizationId UUID REFERENCES organization(id) ON DELETE CASCADE,
570
- module VARCHAR NOT NULL, -- 'GITLAB', 'JIRA', 'YOUR_MODULE'
571
- section VARCHAR NOT NULL, -- 'CREDENTIALS', 'OAUTH', 'PREFERENCES'
572
- key VARCHAR NOT NULL, -- 'TOKEN', 'API_KEY', 'HOST'
573
- value VARCHAR NOT NULL, -- Actual value
574
- type settings_type_enum NOT NULL,
575
- label VARCHAR NOT NULL,
576
- description VARCHAR,
577
- options JSONB,
578
- sortOrder INTEGER DEFAULT 0,
579
- createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
580
- updatedAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
581
- UNIQUE(organizationId, module, section, key)
582
- );
583
- \`\`\`
584
-
585
- ---
586
-
587
- ## ๐Ÿงช Test Server
588
-
589
- The module includes a standalone test server for development and testing.
590
-
591
- ### Start the Test Server
592
-
593
- \`\`\`bash
594
- cd src/organization
595
- npm install
596
- npm run start:dev
597
- \`\`\`
598
-
599
- The server runs on **http://localhost:3002**
600
-
601
- ### Test Endpoints
602
-
603
- \`\`\`bash
604
- # Get all organizations
605
- GET http://localhost:3002/test/organizations
606
-
607
- # Get organization by ID
608
- GET http://localhost:3002/test/organizations/:id
609
-
610
- # Get organization settings
611
- GET http://localhost:3002/test/organizations/:id/settings
612
-
613
- # Get settings for specific module
614
- GET http://localhost:3002/test/organizations/:id/settings?module=GITLAB
615
-
616
- # Create/update setting
617
- PUT http://localhost:3002/test/organizations/:id/settings
618
- Content-Type: application/json
619
-
620
- {
621
- "module": "GITLAB",
622
- "section": "CREDENTIALS",
623
- "key": "TOKEN",
624
- "value": "your-token",
625
- "label": "GitLab Token",
626
- "type": "secret"
627
- }
628
- \`\`\`
629
-
630
- ### Test Controller
631
-
632
- The test controller is for **reference and development only**. In production, implement your own controllers with:
633
- - Custom authentication
634
- - Custom authorization
635
- - Custom validation
636
- - Custom error handling
637
-
638
- **Location:** \`test/controllers/organization-settings-test.controller.ts\`
639
-
640
- ---
641
-
642
- ## ๐Ÿ“– Migration Guide
643
-
644
- ### Running Migrations
645
-
646
- \`\`\`bash
647
- # Generate migration
648
- npm run migration:generate -- YourMigrationName
649
-
650
- # Run migrations
651
- npm run migration:run
652
-
653
- # Revert migration
654
- npm run migration:revert
655
- \`\`\`
656
-
657
- ### Required Migrations
658
-
659
- The module requires these migrations:
660
- 1. Create organizations table
661
- 2. Create organization_users table
662
- 3. Create organization_settings table
663
-
664
- Migrations are located in: \`database/migrations/\`
665
-
666
- ---
667
-
668
- ## ๐Ÿ’ป Examples
669
-
670
- ### Example 1: Create Organization with Owner
671
-
672
- \`\`\`typescript
673
- @Injectable()
674
- export class OnboardingService {
675
- constructor(
676
- private readonly orgService: OrganizationService,
677
- private readonly orgUserService: OrganizationUserService,
678
- ) {}
679
-
680
- async onboardUser(userId: string, companyName: string) {
681
- // Create organization
682
- const org = await this.orgService.createOrganization(
683
- companyName,
684
- this.generateSlug(companyName),
685
- \`\${this.generateSlug(companyName)}.example.com\`,
686
- \`\${companyName} organization\`,
687
- userId
688
- );
689
-
690
- // Owner is automatically added by createOrganization
691
- return org;
692
- }
693
-
694
- private generateSlug(name: string): string {
695
- return name.toLowerCase().replace(/\\s+/g, '-');
696
- }
697
- }
698
- \`\`\`
699
-
700
- ### Example 2: Check User Permission
701
-
702
- \`\`\`typescript
703
- @Controller('organizations/:organizationId/data')
704
- export class DataController {
705
- constructor(
706
- private readonly orgUserService: OrganizationUserService,
707
- ) {}
708
-
709
- @Get()
710
- @UseGuards(OrganizationGuard)
711
- async getData(
712
- @OrganizationId() organizationId: string,
713
- @UserId() userId: string,
714
- ) {
715
- const role = await this.orgUserService.getUserRole(
716
- organizationId,
717
- userId
718
- );
719
-
720
- // Check if user has required role
721
- if (![ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER].includes(role)) {
722
- throw new ForbiddenException('Insufficient permissions');
723
- }
724
-
725
- // Return data
726
- }
727
- }
728
- \`\`\`
729
-
730
- ### Example 3: Module with Organization Settings
731
-
732
- \`\`\`typescript
733
- // GitLab Service
734
- @Injectable()
735
- export class GitLabService {
736
- constructor(
737
- private readonly orgSettings: OrganizationSettingsService
738
- ) {}
739
-
740
- async getClient(organizationId: string) {
741
- // Get org-specific GitLab credentials
742
- const [host, token] = await Promise.all([
743
- this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:HOST'),
744
- this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:TOKEN'),
745
- ]);
746
-
747
- // Validate configuration
748
- if (!host || !token) {
749
- throw new Error(
750
- 'GitLab not configured for this organization. ' +
751
- 'Please configure it in organization settings.'
752
- );
753
- }
754
-
755
- // Return organization-specific client
756
- return new GitLab({ host, token });
757
- }
758
-
759
- async getRepositories(organizationId: string) {
760
- const client = await this.getClient(organizationId);
761
- return client.Projects.all();
762
- }
763
- }
764
- \`\`\`
765
-
766
- ### Example 4: Bulk Configure Organization
767
-
768
- \`\`\`typescript
769
- @Injectable()
770
- export class OrganizationSetupService {
771
- constructor(
772
- private readonly orgSettings: OrganizationSettingsService,
773
- ) {}
774
-
775
- async configureGitLab(
776
- organizationId: string,
777
- config: { host: string; token: string }
778
- ) {
779
- return this.orgSettings.bulkUpdateSettings(organizationId, [
780
- {
781
- module: 'GITLAB',
782
- section: 'CREDENTIALS',
783
- key: 'HOST',
784
- value: config.host,
785
- label: 'GitLab Host',
786
- type: SETTINGS_TYPE.TEXT,
787
- },
788
- {
789
- module: 'GITLAB',
790
- section: 'CREDENTIALS',
791
- key: 'TOKEN',
792
- value: config.token,
793
- label: 'GitLab Token',
794
- type: SETTINGS_TYPE.SECRET,
795
- },
796
- ]);
797
- }
798
- }
799
- \`\`\`
800
-
801
- ### Example 5: Settings Resolution Pattern
802
-
803
- \`\`\`typescript
804
- @Injectable()
805
- export class IntegrationService {
806
- constructor(
807
- private readonly orgSettings: OrganizationSettingsService,
808
- ) {}
809
-
810
- async getIntegrationConfig(organizationId: string, integration: string) {
811
- // Get all settings for this integration
812
- const settings = await this.orgSettings.getAllForOrganization(
813
- organizationId,
814
- integration
815
- );
816
-
817
- // Check if configured
818
- if (settings.length === 0) {
819
- return {
820
- configured: false,
821
- message: \`\${integration} is not configured for this organization\`,
822
- };
823
- }
824
-
825
- // Convert to key-value object
826
- const config = settings.reduce((acc, setting) => {
827
- acc[\`\${setting.section}:\${setting.key}\`] = setting.value;
828
- return acc;
829
- }, {});
830
-
831
- return {
832
- configured: true,
833
- config,
834
- };
835
- }
836
- }
837
- \`\`\`
838
-
839
- ---
840
-
841
- ## ๐Ÿ“ Best Practices
842
-
843
- ### 1. Always Check Configuration
844
-
845
- \`\`\`typescript
846
- // โŒ Don't assume settings exist
847
- const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
848
- await client.connect(token); // Could be undefined!
849
-
850
- // โœ… Always validate
851
- const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
852
- if (!token) {
853
- throw new Error('Module not configured');
854
- }
855
- await client.connect(token);
856
- \`\`\`
857
-
858
- ### 2. Use Bulk Operations
859
-
860
- \`\`\`typescript
861
- // โŒ Multiple single calls
862
- const host = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:HOST');
863
- const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
864
- const secret = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:SECRET');
865
-
866
- // โœ… Single bulk call
867
- const settings = await orgSettings.getManySettings(orgId, [
868
- 'MODULE:CREDENTIALS:HOST',
869
- 'MODULE:CREDENTIALS:TOKEN',
870
- 'MODULE:CREDENTIALS:SECRET',
871
- ]);
872
- \`\`\`
873
-
874
- ### 3. Implement Your Own Controllers
875
-
876
- \`\`\`typescript
877
- // โŒ Don't use test controller in production
878
- import { OrganizationSettingsTestController } from '@venturialstd/organization';
879
-
880
- // โœ… Implement your own
881
- @Controller('api/organizations/:organizationId/settings')
882
- @UseGuards(JwtAuthGuard, OrganizationGuard)
883
- export class OrganizationSettingsController {
884
- // Your implementation with proper auth
885
- }
886
- \`\`\`
887
-
888
- ### 4. Use Type-Safe Settings Types
889
-
890
- \`\`\`typescript
891
- // โœ… Import and use the enum
892
- import { SETTINGS_TYPE } from '@venturialstd/organization';
893
-
894
- const setting = {
895
- type: SETTINGS_TYPE.SECRET, // Type-safe
896
- // ...
897
- };
898
- \`\`\`
899
-
900
- ---
901
-
902
- ## ๐Ÿ“„ License
903
-
904
- MIT
905
-
906
- ---
907
-
908
- ## ๐Ÿค Contributing
909
-
910
- Contributions welcome! Please read the contributing guidelines before submitting PRs.
1
+ # @venturialstd/organization
2
+
3
+ A comprehensive NestJS module for managing organizations with multi-tenant capabilities, role-based access control, and isolated per-organization settings.
4
+
5
+ ## ๐Ÿ“‹ Table of Contents
6
+
7
+ - [Features](#features)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Core Concepts](#core-concepts)
11
+ - [Architecture](#architecture)
12
+ - [API Reference](#api-reference)
13
+ - [Organization Settings System](#organization-settings-system)
14
+ - [Test Server](#test-server)
15
+ - [Migration Guide](#migration-guide)
16
+ - [Examples](#examples)
17
+ - [Best Practices](#best-practices)
18
+
19
+ ---
20
+
21
+ ## โœจ Features
22
+
23
+ - ๐Ÿข **Organization Management**: Complete CRUD operations for organizations
24
+ - ๐Ÿ‘ฅ **Member Management**: Add, invite, and manage organization members
25
+ - ๐Ÿ” **Role-Based Access Control**: Owner, Admin, Member, and Viewer roles
26
+ - โš™๏ธ **Isolated Settings System**: Per-organization configuration for modules
27
+ - ๐Ÿ”’ **Complete Isolation**: No fallback between organizations
28
+ - ๐Ÿ›ก๏ธ **Guards & Decorators**: Request-level access control
29
+ - ๐Ÿ”„ **TypeORM Integration**: Full database support with migrations
30
+ - ๐Ÿ“ฆ **Fully Typed**: Complete TypeScript support
31
+ - ๐Ÿงช **Test Infrastructure**: Standalone test server
32
+
33
+ ---
34
+
35
+ ## ๐Ÿ“ฆ Installation
36
+
37
+ \`\`\`bash
38
+ npm install @venturialstd/organization
39
+ \`\`\`
40
+
41
+ ### Peer Dependencies
42
+
43
+ \`\`\`json
44
+ {
45
+ "@nestjs/common": "^11.0.11",
46
+ "@nestjs/core": "^11.0.5",
47
+ "@nestjs/typeorm": "^10.0.0",
48
+ "@venturialstd/core": "^1.0.16",
49
+ "class-transformer": "^0.5.1",
50
+ "class-validator": "^0.14.1",
51
+ "typeorm": "^0.3.20"
52
+ }
53
+ \`\`\`
54
+
55
+ ---
56
+
57
+ ## ๐Ÿš€ Quick Start
58
+
59
+ ### 1. Import the Module
60
+
61
+ \`\`\`typescript
62
+ import { Module } from '@nestjs/common';
63
+ import { OrganizationModule } from '@venturialstd/organization';
64
+
65
+ @Module({
66
+ imports: [OrganizationModule],
67
+ })
68
+ export class AppModule {}
69
+ \`\`\`
70
+
71
+ ### 2. Run Migrations
72
+
73
+ \`\`\`bash
74
+ # Create and run migrations
75
+ npm run migration:generate -- CreateOrganizations
76
+ npm run migration:run
77
+ \`\`\`
78
+
79
+ ### 3. Use the Services
80
+
81
+ \`\`\`typescript
82
+ import { Injectable } from '@nestjs/common';
83
+ import {
84
+ OrganizationService,
85
+ OrganizationUserService,
86
+ ORGANIZATION_USER_ROLE
87
+ } from '@venturialstd/organization';
88
+
89
+ @Injectable()
90
+ export class YourService {
91
+ constructor(
92
+ private readonly organizationService: OrganizationService,
93
+ private readonly organizationUserService: OrganizationUserService,
94
+ ) {}
95
+
96
+ async createOrganization(userId: string) {
97
+ return this.organizationService.createOrganization(
98
+ 'My Organization',
99
+ 'my-org',
100
+ 'example.com',
101
+ 'Description',
102
+ userId
103
+ );
104
+ }
105
+
106
+ async addMember(organizationId: string, userId: string) {
107
+ return this.organizationUserService.addUserToOrganization(
108
+ organizationId,
109
+ userId,
110
+ ORGANIZATION_USER_ROLE.MEMBER
111
+ );
112
+ }
113
+ }
114
+ \`\`\`
115
+
116
+ ---
117
+
118
+ ## ๐Ÿ’ก Core Concepts
119
+
120
+ ### Organizations
121
+
122
+ An organization represents a tenant in your multi-tenant application. Each organization:
123
+ - Has a unique name and slug
124
+ - Can have a custom domain
125
+ - Has its own settings and configuration
126
+ - Contains multiple members with different roles
127
+ - Is completely isolated from other organizations
128
+
129
+ ### Organization Members
130
+
131
+ Users can belong to multiple organizations with different roles:
132
+ - **Owner**: Full control, can delete organization
133
+ - **Admin**: Can manage members and settings
134
+ - **Member**: Standard access to organization resources
135
+ - **Viewer**: Read-only access
136
+
137
+ ### Organization Settings
138
+
139
+ Each organization can have isolated settings for any module in your application:
140
+ - **Complete Isolation**: No sharing between organizations
141
+ - **No Fallback**: Each organization must configure its own settings
142
+ - **Type-Safe**: Fully typed with TypeScript
143
+ - **Module-Agnostic**: Works with any module (GitLab, Jira, Slack, etc.)
144
+
145
+ ---
146
+
147
+ ## ๐Ÿ—๏ธ Architecture
148
+
149
+ ### Module Structure
150
+
151
+ \`\`\`
152
+ OrganizationModule
153
+ โ”œโ”€โ”€ Entities
154
+ โ”‚ โ”œโ”€โ”€ Organization # Organization entity
155
+ โ”‚ โ”œโ”€โ”€ OrganizationUser # User-Organization relationship
156
+ โ”‚ โ””โ”€โ”€ OrganizationSettings # Per-organization settings
157
+ โ”œโ”€โ”€ Services
158
+ โ”‚ โ”œโ”€โ”€ OrganizationService # CRUD operations
159
+ โ”‚ โ”œโ”€โ”€ OrganizationUserService # Member management
160
+ โ”‚ โ””โ”€โ”€ OrganizationSettingsService # Settings management
161
+ โ”œโ”€โ”€ Guards
162
+ โ”‚ โ”œโ”€โ”€ OrganizationGuard # Check user belongs to org
163
+ โ”‚ โ””โ”€โ”€ OrganizationRoleGuard # Check user role
164
+ โ””โ”€โ”€ Decorators
165
+ โ”œโ”€โ”€ @OrganizationId() # Extract org ID from request
166
+ โ”œโ”€โ”€ @UserId() # Extract user ID from request
167
+ โ”œโ”€โ”€ @Roles() # Define required roles
168
+ โ””โ”€โ”€ @OrganizationContext() # Extract full context
169
+ \`\`\`
170
+
171
+ ### Settings Isolation
172
+
173
+ \`\`\`
174
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
175
+ โ”‚ Application โ”‚
176
+ โ”‚ โ”‚
177
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
178
+ โ”‚ โ”‚ Organization A โ”‚ โ”‚
179
+ โ”‚ โ”‚ โ€ข GitLab: gitlab-a.com + token-a โ”‚ โ”‚
180
+ โ”‚ โ”‚ โ€ข Jira: jira-a.com + credentials-a โ”‚ โ”‚
181
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
182
+ โ”‚ โ”‚
183
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
184
+ โ”‚ โ”‚ Organization B โ”‚ โ”‚
185
+ โ”‚ โ”‚ โ€ข GitLab: gitlab.com + token-b โ”‚ โ”‚
186
+ โ”‚ โ”‚ โ€ข Jira: [Not configured] โ”‚ โ”‚
187
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
188
+ โ”‚ โ”‚
189
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
190
+ \`\`\`
191
+
192
+ ---
193
+
194
+ ## ๐Ÿ“š API Reference
195
+
196
+ ### Services
197
+
198
+ #### OrganizationService
199
+
200
+ \`\`\`typescript
201
+ class OrganizationService {
202
+ // Create a new organization
203
+ createOrganization(
204
+ name: string,
205
+ slug: string,
206
+ domain: string,
207
+ description: string,
208
+ ownerId: string
209
+ ): Promise<Organization>
210
+
211
+ // Get organization by ID
212
+ getOrganizationById(id: string): Promise<Organization>
213
+
214
+ // Update organization
215
+ updateOrganization(id: string, data: Partial<Organization>): Promise<Organization>
216
+
217
+ // Delete organization
218
+ deleteOrganization(id: string): Promise<void>
219
+
220
+ // Get user's organizations
221
+ getUserOrganizations(userId: string): Promise<Organization[]>
222
+ }
223
+ \`\`\`
224
+
225
+ #### OrganizationUserService
226
+
227
+ \`\`\`typescript
228
+ class OrganizationUserService {
229
+ // Add user to organization
230
+ addUserToOrganization(
231
+ organizationId: string,
232
+ userId: string,
233
+ role: ORGANIZATION_USER_ROLE
234
+ ): Promise<OrganizationUser>
235
+
236
+ // Remove user from organization
237
+ removeUserFromOrganization(
238
+ organizationId: string,
239
+ userId: string
240
+ ): Promise<void>
241
+
242
+ // Update user role
243
+ updateUserRole(
244
+ organizationId: string,
245
+ userId: string,
246
+ role: ORGANIZATION_USER_ROLE
247
+ ): Promise<OrganizationUser>
248
+
249
+ // Get organization members
250
+ getOrganizationMembers(organizationId: string): Promise<OrganizationUser[]>
251
+
252
+ // Check user role
253
+ getUserRole(
254
+ organizationId: string,
255
+ userId: string
256
+ ): Promise<ORGANIZATION_USER_ROLE>
257
+ }
258
+ \`\`\`
259
+
260
+ #### OrganizationSettingsService
261
+
262
+ \`\`\`typescript
263
+ class OrganizationSettingsService {
264
+ // Get a single setting
265
+ get(organizationId: string, key: string): Promise<string | undefined>
266
+
267
+ // Get multiple settings
268
+ getManySettings(
269
+ organizationId: string,
270
+ keys: string[]
271
+ ): Promise<Record<string, string>>
272
+
273
+ // Get all settings for organization
274
+ getAllForOrganization(
275
+ organizationId: string,
276
+ module?: string
277
+ ): Promise<OrganizationSettings[]>
278
+
279
+ // Set or update a setting
280
+ setOrganizationSetting(
281
+ organizationId: string,
282
+ dto: Partial<OrganizationSettings>
283
+ ): Promise<OrganizationSettings>
284
+
285
+ // Delete a setting
286
+ deleteOrganizationSetting(
287
+ organizationId: string,
288
+ module: string,
289
+ section: string,
290
+ key: string
291
+ ): Promise<void>
292
+
293
+ // Reset all settings for a module
294
+ resetModuleSettings(
295
+ organizationId: string,
296
+ module: string
297
+ ): Promise<void>
298
+
299
+ // Bulk update settings
300
+ bulkUpdateSettings(
301
+ organizationId: string,
302
+ settings: Partial<OrganizationSettings>[]
303
+ ): Promise<OrganizationSettings[]>
304
+ }
305
+ \`\`\`
306
+
307
+ ### Entities
308
+
309
+ #### Organization
310
+
311
+ \`\`\`typescript
312
+ class Organization {
313
+ id: string;
314
+ name: string;
315
+ slug: string;
316
+ domain: string;
317
+ description: string;
318
+ isActive: boolean;
319
+ settings: Record<string, unknown>;
320
+ ownerId: string;
321
+ createdAt: Date;
322
+ updatedAt: Date;
323
+ }
324
+ \`\`\`
325
+
326
+ #### OrganizationUser
327
+
328
+ \`\`\`typescript
329
+ class OrganizationUser {
330
+ id: string;
331
+ organizationId: string;
332
+ userId: string;
333
+ role: ORGANIZATION_USER_ROLE;
334
+ isActive: boolean;
335
+ createdAt: Date;
336
+ updatedAt: Date;
337
+ }
338
+ \`\`\`
339
+
340
+ #### OrganizationSettings
341
+
342
+ \`\`\`typescript
343
+ class OrganizationSettings {
344
+ id: string;
345
+ organizationId: string;
346
+ module: string; // e.g., 'GITLAB', 'JIRA'
347
+ section: string; // e.g., 'CREDENTIALS', 'OAUTH'
348
+ key: string; // e.g., 'TOKEN', 'API_KEY'
349
+ value: string;
350
+ type: SETTINGS_TYPE;
351
+ label: string;
352
+ description: string;
353
+ options: object | null;
354
+ sortOrder: number;
355
+ createdAt: Date;
356
+ updatedAt: Date;
357
+ }
358
+ \`\`\`
359
+
360
+ ### Decorators
361
+
362
+ \`\`\`typescript
363
+ // Extract organization ID from request
364
+ @Get()
365
+ getData(@OrganizationId() organizationId: string) { }
366
+
367
+ // Extract user ID from request
368
+ @Get()
369
+ getData(@UserId() userId: string) { }
370
+
371
+ // Extract full context
372
+ @Get()
373
+ getData(@OrganizationContext() context: OrganizationContextType) { }
374
+
375
+ // Define required roles
376
+ @Roles(ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER)
377
+ @UseGuards(OrganizationRoleGuard)
378
+ adminEndpoint() { }
379
+ \`\`\`
380
+
381
+ ### Guards
382
+
383
+ \`\`\`typescript
384
+ // Check user belongs to organization
385
+ @UseGuards(OrganizationGuard)
386
+ @Get('organizations/:organizationId/data')
387
+ getData() { }
388
+
389
+ // Check user has required role
390
+ @Roles(ORGANIZATION_USER_ROLE.ADMIN)
391
+ @UseGuards(OrganizationRoleGuard)
392
+ adminEndpoint() { }
393
+ \`\`\`
394
+
395
+ ### Constants
396
+
397
+ \`\`\`typescript
398
+ // User roles
399
+ enum ORGANIZATION_USER_ROLE {
400
+ OWNER = 'owner',
401
+ ADMIN = 'admin',
402
+ MEMBER = 'member',
403
+ VIEWER = 'viewer',
404
+ }
405
+
406
+ // Settings types
407
+ enum SETTINGS_TYPE {
408
+ TEXT = 'text',
409
+ TEXTAREA = 'textarea',
410
+ NUMBER = 'number',
411
+ BOOLEAN = 'boolean',
412
+ SELECT = 'select',
413
+ MULTISELECT = 'multiselect',
414
+ JSON = 'json',
415
+ DATE = 'date',
416
+ SECRET = 'secret',
417
+ }
418
+ \`\`\`
419
+
420
+ ---
421
+
422
+ ## โš™๏ธ Organization Settings System
423
+
424
+ ### Complete Isolation
425
+
426
+ Organization settings are **completely isolated** with no fallback mechanism:
427
+
428
+ \`\`\`typescript
429
+ // Organization A
430
+ const tokenA = await orgSettings.get('org-a-id', 'GITLAB:CREDENTIALS:TOKEN');
431
+ // Returns: 'token-a'
432
+
433
+ // Organization B (not configured)
434
+ const tokenB = await orgSettings.get('org-b-id', 'GITLAB:CREDENTIALS:TOKEN');
435
+ // Returns: undefined (NO FALLBACK)
436
+ \`\`\`
437
+
438
+ ### Settings Types
439
+
440
+ Use \`SETTINGS_TYPE\` enum for type-safe settings:
441
+
442
+ \`\`\`typescript
443
+ import { SETTINGS_TYPE } from '@venturialstd/organization';
444
+
445
+ const settings = [
446
+ {
447
+ module: 'GITLAB',
448
+ section: 'CREDENTIALS',
449
+ key: 'TOKEN',
450
+ type: SETTINGS_TYPE.SECRET, // Hides value in UI
451
+ // ...
452
+ },
453
+ {
454
+ module: 'GITLAB',
455
+ section: 'PREFERENCES',
456
+ key: 'AUTO_MERGE',
457
+ type: SETTINGS_TYPE.BOOLEAN, // true/false toggle
458
+ // ...
459
+ },
460
+ ];
461
+ \`\`\`
462
+
463
+ ### Using Settings in Your Module
464
+
465
+ #### 1. Define Settings for Your Module
466
+
467
+ \`\`\`typescript
468
+ // src/your-module/settings/your-module.settings.ts
469
+ import { SETTINGS_TYPE } from '@venturialstd/organization';
470
+
471
+ export const SETTINGS = [
472
+ {
473
+ scope: 'ORGANIZATION',
474
+ module: 'YOUR_MODULE',
475
+ section: 'CREDENTIALS',
476
+ key: 'API_KEY',
477
+ label: 'API Key',
478
+ description: 'Your module API key',
479
+ type: SETTINGS_TYPE.SECRET,
480
+ value: '',
481
+ options: {
482
+ placeholder: 'Enter your API key',
483
+ help: 'Get your API key from your module dashboard',
484
+ },
485
+ sortOrder: 10,
486
+ },
487
+ {
488
+ scope: 'ORGANIZATION',
489
+ module: 'YOUR_MODULE',
490
+ section: 'CREDENTIALS',
491
+ key: 'API_SECRET',
492
+ label: 'API Secret',
493
+ description: 'Your module API secret',
494
+ type: SETTINGS_TYPE.SECRET,
495
+ value: '',
496
+ options: null,
497
+ sortOrder: 11,
498
+ },
499
+ ];
500
+ \`\`\`
501
+
502
+ #### 2. Use Settings in Your Service
503
+
504
+ \`\`\`typescript
505
+ import { Injectable } from '@nestjs/common';
506
+ import { OrganizationSettingsService } from '@venturialstd/organization';
507
+
508
+ @Injectable()
509
+ export class YourModuleService {
510
+ constructor(
511
+ private readonly orgSettings: OrganizationSettingsService
512
+ ) {}
513
+
514
+ async connect(organizationId: string) {
515
+ // Get organization-specific credentials
516
+ const apiKey = await this.orgSettings.get(
517
+ organizationId,
518
+ 'YOUR_MODULE:CREDENTIALS:API_KEY'
519
+ );
520
+
521
+ const apiSecret = await this.orgSettings.get(
522
+ organizationId,
523
+ 'YOUR_MODULE:CREDENTIALS:API_SECRET'
524
+ );
525
+
526
+ // Check if configured
527
+ if (!apiKey || !apiSecret) {
528
+ throw new Error('Module not configured for this organization');
529
+ }
530
+
531
+ // Use organization-specific credentials
532
+ return this.client.connect({ apiKey, apiSecret });
533
+ }
534
+
535
+ async getMultipleSettings(organizationId: string) {
536
+ // Get multiple settings at once
537
+ const settings = await this.orgSettings.getManySettings(
538
+ organizationId,
539
+ [
540
+ 'YOUR_MODULE:CREDENTIALS:API_KEY',
541
+ 'YOUR_MODULE:CREDENTIALS:API_SECRET',
542
+ 'YOUR_MODULE:PREFERENCES:TIMEOUT',
543
+ ]
544
+ );
545
+
546
+ return settings;
547
+ }
548
+ }
549
+ \`\`\`
550
+
551
+ #### 3. Import OrganizationModule
552
+
553
+ \`\`\`typescript
554
+ import { Module } from '@nestjs/common';
555
+ import { OrganizationModule } from '@venturialstd/organization';
556
+
557
+ @Module({
558
+ imports: [OrganizationModule],
559
+ providers: [YourModuleService],
560
+ })
561
+ export class YourModule {}
562
+ \`\`\`
563
+
564
+ ### Database Schema
565
+
566
+ \`\`\`sql
567
+ CREATE TABLE organization_settings (
568
+ id UUID PRIMARY KEY,
569
+ organizationId UUID REFERENCES organization(id) ON DELETE CASCADE,
570
+ module VARCHAR NOT NULL, -- 'GITLAB', 'JIRA', 'YOUR_MODULE'
571
+ section VARCHAR NOT NULL, -- 'CREDENTIALS', 'OAUTH', 'PREFERENCES'
572
+ key VARCHAR NOT NULL, -- 'TOKEN', 'API_KEY', 'HOST'
573
+ value VARCHAR NOT NULL, -- Actual value
574
+ type settings_type_enum NOT NULL,
575
+ label VARCHAR NOT NULL,
576
+ description VARCHAR,
577
+ options JSONB,
578
+ sortOrder INTEGER DEFAULT 0,
579
+ createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
580
+ updatedAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
581
+ UNIQUE(organizationId, module, section, key)
582
+ );
583
+ \`\`\`
584
+
585
+ ---
586
+
587
+ ## ๐Ÿงช Test Server
588
+
589
+ The module includes a standalone test server for development and testing.
590
+
591
+ ### Start the Test Server
592
+
593
+ \`\`\`bash
594
+ cd src/organization
595
+ npm install
596
+ npm run start:dev
597
+ \`\`\`
598
+
599
+ The server runs on **http://localhost:3002**
600
+
601
+ ### Test Endpoints
602
+
603
+ \`\`\`bash
604
+ # Get all organizations
605
+ GET http://localhost:3002/test/organizations
606
+
607
+ # Get organization by ID
608
+ GET http://localhost:3002/test/organizations/:id
609
+
610
+ # Get organization settings
611
+ GET http://localhost:3002/test/organizations/:id/settings
612
+
613
+ # Get settings for specific module
614
+ GET http://localhost:3002/test/organizations/:id/settings?module=GITLAB
615
+
616
+ # Create/update setting
617
+ PUT http://localhost:3002/test/organizations/:id/settings
618
+ Content-Type: application/json
619
+
620
+ {
621
+ "module": "GITLAB",
622
+ "section": "CREDENTIALS",
623
+ "key": "TOKEN",
624
+ "value": "your-token",
625
+ "label": "GitLab Token",
626
+ "type": "secret"
627
+ }
628
+ \`\`\`
629
+
630
+ ### Test Controller
631
+
632
+ The test controller is for **reference and development only**. In production, implement your own controllers with:
633
+ - Custom authentication
634
+ - Custom authorization
635
+ - Custom validation
636
+ - Custom error handling
637
+
638
+ **Location:** \`test/controllers/organization-settings-test.controller.ts\`
639
+
640
+ ---
641
+
642
+ ## ๐Ÿ“– Migration Guide
643
+
644
+ ### Running Migrations
645
+
646
+ \`\`\`bash
647
+ # Generate migration
648
+ npm run migration:generate -- YourMigrationName
649
+
650
+ # Run migrations
651
+ npm run migration:run
652
+
653
+ # Revert migration
654
+ npm run migration:revert
655
+ \`\`\`
656
+
657
+ ### Required Migrations
658
+
659
+ The module requires these migrations:
660
+ 1. Create organizations table
661
+ 2. Create organization_users table
662
+ 3. Create organization_settings table
663
+
664
+ Migrations are located in: \`database/migrations/\`
665
+
666
+ ---
667
+
668
+ ## ๐Ÿ’ป Examples
669
+
670
+ ### Example 1: Create Organization with Owner
671
+
672
+ \`\`\`typescript
673
+ @Injectable()
674
+ export class OnboardingService {
675
+ constructor(
676
+ private readonly orgService: OrganizationService,
677
+ private readonly orgUserService: OrganizationUserService,
678
+ ) {}
679
+
680
+ async onboardUser(userId: string, companyName: string) {
681
+ // Create organization
682
+ const org = await this.orgService.createOrganization(
683
+ companyName,
684
+ this.generateSlug(companyName),
685
+ \`\${this.generateSlug(companyName)}.example.com\`,
686
+ \`\${companyName} organization\`,
687
+ userId
688
+ );
689
+
690
+ // Owner is automatically added by createOrganization
691
+ return org;
692
+ }
693
+
694
+ private generateSlug(name: string): string {
695
+ return name.toLowerCase().replace(/\\s+/g, '-');
696
+ }
697
+ }
698
+ \`\`\`
699
+
700
+ ### Example 2: Check User Permission
701
+
702
+ \`\`\`typescript
703
+ @Controller('organizations/:organizationId/data')
704
+ export class DataController {
705
+ constructor(
706
+ private readonly orgUserService: OrganizationUserService,
707
+ ) {}
708
+
709
+ @Get()
710
+ @UseGuards(OrganizationGuard)
711
+ async getData(
712
+ @OrganizationId() organizationId: string,
713
+ @UserId() userId: string,
714
+ ) {
715
+ const role = await this.orgUserService.getUserRole(
716
+ organizationId,
717
+ userId
718
+ );
719
+
720
+ // Check if user has required role
721
+ if (![ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER].includes(role)) {
722
+ throw new ForbiddenException('Insufficient permissions');
723
+ }
724
+
725
+ // Return data
726
+ }
727
+ }
728
+ \`\`\`
729
+
730
+ ### Example 3: Module with Organization Settings
731
+
732
+ \`\`\`typescript
733
+ // GitLab Service
734
+ @Injectable()
735
+ export class GitLabService {
736
+ constructor(
737
+ private readonly orgSettings: OrganizationSettingsService
738
+ ) {}
739
+
740
+ async getClient(organizationId: string) {
741
+ // Get org-specific GitLab credentials
742
+ const [host, token] = await Promise.all([
743
+ this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:HOST'),
744
+ this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:TOKEN'),
745
+ ]);
746
+
747
+ // Validate configuration
748
+ if (!host || !token) {
749
+ throw new Error(
750
+ 'GitLab not configured for this organization. ' +
751
+ 'Please configure it in organization settings.'
752
+ );
753
+ }
754
+
755
+ // Return organization-specific client
756
+ return new GitLab({ host, token });
757
+ }
758
+
759
+ async getRepositories(organizationId: string) {
760
+ const client = await this.getClient(organizationId);
761
+ return client.Projects.all();
762
+ }
763
+ }
764
+ \`\`\`
765
+
766
+ ### Example 4: Bulk Configure Organization
767
+
768
+ \`\`\`typescript
769
+ @Injectable()
770
+ export class OrganizationSetupService {
771
+ constructor(
772
+ private readonly orgSettings: OrganizationSettingsService,
773
+ ) {}
774
+
775
+ async configureGitLab(
776
+ organizationId: string,
777
+ config: { host: string; token: string }
778
+ ) {
779
+ return this.orgSettings.bulkUpdateSettings(organizationId, [
780
+ {
781
+ module: 'GITLAB',
782
+ section: 'CREDENTIALS',
783
+ key: 'HOST',
784
+ value: config.host,
785
+ label: 'GitLab Host',
786
+ type: SETTINGS_TYPE.TEXT,
787
+ },
788
+ {
789
+ module: 'GITLAB',
790
+ section: 'CREDENTIALS',
791
+ key: 'TOKEN',
792
+ value: config.token,
793
+ label: 'GitLab Token',
794
+ type: SETTINGS_TYPE.SECRET,
795
+ },
796
+ ]);
797
+ }
798
+ }
799
+ \`\`\`
800
+
801
+ ### Example 5: Settings Resolution Pattern
802
+
803
+ \`\`\`typescript
804
+ @Injectable()
805
+ export class IntegrationService {
806
+ constructor(
807
+ private readonly orgSettings: OrganizationSettingsService,
808
+ ) {}
809
+
810
+ async getIntegrationConfig(organizationId: string, integration: string) {
811
+ // Get all settings for this integration
812
+ const settings = await this.orgSettings.getAllForOrganization(
813
+ organizationId,
814
+ integration
815
+ );
816
+
817
+ // Check if configured
818
+ if (settings.length === 0) {
819
+ return {
820
+ configured: false,
821
+ message: \`\${integration} is not configured for this organization\`,
822
+ };
823
+ }
824
+
825
+ // Convert to key-value object
826
+ const config = settings.reduce((acc, setting) => {
827
+ acc[\`\${setting.section}:\${setting.key}\`] = setting.value;
828
+ return acc;
829
+ }, {});
830
+
831
+ return {
832
+ configured: true,
833
+ config,
834
+ };
835
+ }
836
+ }
837
+ \`\`\`
838
+
839
+ ---
840
+
841
+ ## ๐Ÿ“ Best Practices
842
+
843
+ ### 1. Always Check Configuration
844
+
845
+ \`\`\`typescript
846
+ // โŒ Don't assume settings exist
847
+ const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
848
+ await client.connect(token); // Could be undefined!
849
+
850
+ // โœ… Always validate
851
+ const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
852
+ if (!token) {
853
+ throw new Error('Module not configured');
854
+ }
855
+ await client.connect(token);
856
+ \`\`\`
857
+
858
+ ### 2. Use Bulk Operations
859
+
860
+ \`\`\`typescript
861
+ // โŒ Multiple single calls
862
+ const host = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:HOST');
863
+ const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
864
+ const secret = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:SECRET');
865
+
866
+ // โœ… Single bulk call
867
+ const settings = await orgSettings.getManySettings(orgId, [
868
+ 'MODULE:CREDENTIALS:HOST',
869
+ 'MODULE:CREDENTIALS:TOKEN',
870
+ 'MODULE:CREDENTIALS:SECRET',
871
+ ]);
872
+ \`\`\`
873
+
874
+ ### 3. Implement Your Own Controllers
875
+
876
+ \`\`\`typescript
877
+ // โŒ Don't use test controller in production
878
+ import { OrganizationSettingsTestController } from '@venturialstd/organization';
879
+
880
+ // โœ… Implement your own
881
+ @Controller('api/organizations/:organizationId/settings')
882
+ @UseGuards(JwtAuthGuard, OrganizationGuard)
883
+ export class OrganizationSettingsController {
884
+ // Your implementation with proper auth
885
+ }
886
+ \`\`\`
887
+
888
+ ### 4. Use Type-Safe Settings Types
889
+
890
+ \`\`\`typescript
891
+ // โœ… Import and use the enum
892
+ import { SETTINGS_TYPE } from '@venturialstd/organization';
893
+
894
+ const setting = {
895
+ type: SETTINGS_TYPE.SECRET, // Type-safe
896
+ // ...
897
+ };
898
+ \`\`\`
899
+
900
+ ---
901
+
902
+ ## ๐Ÿ“„ License
903
+
904
+ MIT
905
+
906
+ ---
907
+
908
+ ## ๐Ÿค Contributing
909
+
910
+ Contributions welcome! Please read the contributing guidelines before submitting PRs.