@venturialstd/organization
v0.0.14
Published
Organization Management Module for Venturial
Keywords
Readme
@venturialstd/organization
A comprehensive NestJS module for managing organizations with multi-tenant capabilities, role-based access control, and isolated per-organization settings.
📋 Table of Contents
- Features
- Installation
- Quick Start
- Core Concepts
- Architecture
- API Reference
- Organization Settings System
- Test Server
- Migration Guide
- Examples
- Best Practices
✨ Features
- 🏢 Organization Management: Complete CRUD operations for organizations
- 👥 Member Management: Add, invite, and manage organization members
- 🔐 Role-Based Access Control: Owner, Admin, Member, and Viewer roles
- ⚙️ Isolated Settings System: Per-organization configuration for modules
- 🔒 Complete Isolation: No fallback between organizations
- 🛡️ Guards & Decorators: Request-level access control
- 🔄 TypeORM Integration: Full database support with migrations
- 📦 Fully Typed: Complete TypeScript support
- 🧪 Test Infrastructure: Standalone test server
📦 Installation
```bash npm install @venturialstd/organization ```
Peer Dependencies
```json { "@nestjs/common": "^11.0.11", "@nestjs/core": "^11.0.5", "@nestjs/typeorm": "^10.0.0", "@venturialstd/core": "^1.0.16", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "typeorm": "^0.3.20" } ```
🚀 Quick Start
1. Import the Module
```typescript import { Module } from '@nestjs/common'; import { OrganizationModule } from '@venturialstd/organization';
@Module({ imports: [OrganizationModule], }) export class AppModule {} ```
2. Run Migrations
```bash
Create and run migrations
npm run migration:generate -- CreateOrganizations npm run migration:run ```
3. Use the Services
```typescript import { Injectable } from '@nestjs/common'; import { OrganizationService, OrganizationUserService, ORGANIZATION_USER_ROLE } from '@venturialstd/organization';
@Injectable() export class YourService { constructor( private readonly organizationService: OrganizationService, private readonly organizationUserService: OrganizationUserService, ) {}
async createOrganization(userId: string) { return this.organizationService.createOrganization( 'My Organization', 'my-org', 'example.com', 'Description', userId ); }
async addMember(organizationId: string, userId: string) { return this.organizationUserService.addUserToOrganization( organizationId, userId, ORGANIZATION_USER_ROLE.MEMBER ); } } ```
💡 Core Concepts
Organizations
An organization represents a tenant in your multi-tenant application. Each organization:
- Has a unique name and slug
- Can have a custom domain
- Has its own settings and configuration
- Contains multiple members with different roles
- Is completely isolated from other organizations
Organization Members
Users can belong to multiple organizations with different roles:
- Owner: Full control, can delete organization
- Admin: Can manage members and settings
- Member: Standard access to organization resources
- Viewer: Read-only access
Organization Settings
Each organization can have isolated settings for any module in your application:
- Complete Isolation: No sharing between organizations
- No Fallback: Each organization must configure its own settings
- Type-Safe: Fully typed with TypeScript
- Module-Agnostic: Works with any module (GitLab, Jira, Slack, etc.)
🏗️ Architecture
Module Structure
``` OrganizationModule ├── Entities │ ├── Organization # Organization entity │ ├── OrganizationUser # User-Organization relationship │ └── OrganizationSettings # Per-organization settings ├── Services │ ├── OrganizationService # CRUD operations │ ├── OrganizationUserService # Member management │ └── OrganizationSettingsService # Settings management ├── Guards │ ├── OrganizationGuard # Check user belongs to org │ └── OrganizationRoleGuard # Check user role └── Decorators ├── @OrganizationId() # Extract org ID from request ├── @UserId() # Extract user ID from request ├── @Roles() # Define required roles └── @OrganizationContext() # Extract full context ```
Settings Isolation
``` ┌─────────────────────────────────────────────────┐ │ Application │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ Organization A │ │ │ │ • GitLab: gitlab-a.com + token-a │ │ │ │ • Jira: jira-a.com + credentials-a │ │ │ └──────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ Organization B │ │ │ │ • GitLab: gitlab.com + token-b │ │ │ │ • Jira: [Not configured] │ │ │ └──────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘ ```
📚 API Reference
Services
OrganizationService
```typescript class OrganizationService { // Create a new organization createOrganization( name: string, slug: string, domain: string, description: string, ownerId: string ): Promise
// Get organization by ID getOrganizationById(id: string): Promise
// Update organization updateOrganization(id: string, data: Partial): Promise
// Delete organization deleteOrganization(id: string): Promise
// Get user's organizations getUserOrganizations(userId: string): Promise<Organization[]> } ```
OrganizationUserService
```typescript class OrganizationUserService { // Add user to organization addUserToOrganization( organizationId: string, userId: string, role: ORGANIZATION_USER_ROLE ): Promise
// Remove user from organization removeUserFromOrganization( organizationId: string, userId: string ): Promise
// Update user role updateUserRole( organizationId: string, userId: string, role: ORGANIZATION_USER_ROLE ): Promise
// Get organization members getOrganizationMembers(organizationId: string): Promise<OrganizationUser[]>
// Check user role getUserRole( organizationId: string, userId: string ): Promise<ORGANIZATION_USER_ROLE> } ```
OrganizationSettingsService
```typescript class OrganizationSettingsService { // Get a single setting get(organizationId: string, key: string): Promise<string | undefined>
// Get multiple settings getManySettings( organizationId: string, keys: string[] ): Promise<Record<string, string>>
// Get all settings for organization getAllForOrganization( organizationId: string, module?: string ): Promise<OrganizationSettings[]>
// Set or update a setting setOrganizationSetting( organizationId: string, dto: Partial ): Promise
// Delete a setting deleteOrganizationSetting( organizationId: string, module: string, section: string, key: string ): Promise
// Reset all settings for a module resetModuleSettings( organizationId: string, module: string ): Promise
// Bulk update settings bulkUpdateSettings( organizationId: string, settings: Partial[] ): Promise<OrganizationSettings[]> } ```
Entities
Organization
```typescript class Organization { id: string; name: string; slug: string; domain: string; description: string; isActive: boolean; settings: Record<string, unknown>; ownerId: string; createdAt: Date; updatedAt: Date; } ```
OrganizationUser
```typescript class OrganizationUser { id: string; organizationId: string; userId: string; role: ORGANIZATION_USER_ROLE; isActive: boolean; createdAt: Date; updatedAt: Date; } ```
OrganizationSettings
```typescript class OrganizationSettings { id: string; organizationId: string; module: string; // e.g., 'GITLAB', 'JIRA' section: string; // e.g., 'CREDENTIALS', 'OAUTH' key: string; // e.g., 'TOKEN', 'API_KEY' value: string; type: SETTINGS_TYPE; label: string; description: string; options: object | null; sortOrder: number; createdAt: Date; updatedAt: Date; } ```
Decorators
```typescript // Extract organization ID from request @Get() getData(@OrganizationId() organizationId: string) { }
// Extract user ID from request @Get() getData(@UserId() userId: string) { }
// Extract full context @Get() getData(@OrganizationContext() context: OrganizationContextType) { }
// Define required roles @Roles(ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER) @UseGuards(OrganizationRoleGuard) adminEndpoint() { } ```
Guards
```typescript // Check user belongs to organization @UseGuards(OrganizationGuard) @Get('organizations/:organizationId/data') getData() { }
// Check user has required role @Roles(ORGANIZATION_USER_ROLE.ADMIN) @UseGuards(OrganizationRoleGuard) adminEndpoint() { } ```
Constants
```typescript // User roles enum ORGANIZATION_USER_ROLE { OWNER = 'owner', ADMIN = 'admin', MEMBER = 'member', VIEWER = 'viewer', }
// Settings types enum SETTINGS_TYPE { TEXT = 'text', TEXTAREA = 'textarea', NUMBER = 'number', BOOLEAN = 'boolean', SELECT = 'select', MULTISELECT = 'multiselect', JSON = 'json', DATE = 'date', SECRET = 'secret', } ```
⚙️ Organization Settings System
Complete Isolation
Organization settings are completely isolated with no fallback mechanism:
```typescript // Organization A const tokenA = await orgSettings.get('org-a-id', 'GITLAB:CREDENTIALS:TOKEN'); // Returns: 'token-a'
// Organization B (not configured) const tokenB = await orgSettings.get('org-b-id', 'GITLAB:CREDENTIALS:TOKEN'); // Returns: undefined (NO FALLBACK) ```
Settings Types
Use `SETTINGS_TYPE` enum for type-safe settings:
```typescript import { SETTINGS_TYPE } from '@venturialstd/organization';
const settings = [ { module: 'GITLAB', section: 'CREDENTIALS', key: 'TOKEN', type: SETTINGS_TYPE.SECRET, // Hides value in UI // ... }, { module: 'GITLAB', section: 'PREFERENCES', key: 'AUTO_MERGE', type: SETTINGS_TYPE.BOOLEAN, // true/false toggle // ... }, ]; ```
Using Settings in Your Module
1. Define Settings for Your Module
```typescript // src/your-module/settings/your-module.settings.ts import { SETTINGS_TYPE } from '@venturialstd/organization';
export const SETTINGS = [ { scope: 'ORGANIZATION', module: 'YOUR_MODULE', section: 'CREDENTIALS', key: 'API_KEY', label: 'API Key', description: 'Your module API key', type: SETTINGS_TYPE.SECRET, value: '', options: { placeholder: 'Enter your API key', help: 'Get your API key from your module dashboard', }, sortOrder: 10, }, { scope: 'ORGANIZATION', module: 'YOUR_MODULE', section: 'CREDENTIALS', key: 'API_SECRET', label: 'API Secret', description: 'Your module API secret', type: SETTINGS_TYPE.SECRET, value: '', options: null, sortOrder: 11, }, ]; ```
2. Use Settings in Your Service
```typescript import { Injectable } from '@nestjs/common'; import { OrganizationSettingsService } from '@venturialstd/organization';
@Injectable() export class YourModuleService { constructor( private readonly orgSettings: OrganizationSettingsService ) {}
async connect(organizationId: string) { // Get organization-specific credentials const apiKey = await this.orgSettings.get( organizationId, 'YOUR_MODULE:CREDENTIALS:API_KEY' );
const apiSecret = await this.orgSettings.get(
organizationId,
'YOUR_MODULE:CREDENTIALS:API_SECRET'
);
// Check if configured
if (!apiKey || !apiSecret) {
throw new Error('Module not configured for this organization');
}
// Use organization-specific credentials
return this.client.connect({ apiKey, apiSecret });}
async getMultipleSettings(organizationId: string) { // Get multiple settings at once const settings = await this.orgSettings.getManySettings( organizationId, [ 'YOUR_MODULE:CREDENTIALS:API_KEY', 'YOUR_MODULE:CREDENTIALS:API_SECRET', 'YOUR_MODULE:PREFERENCES:TIMEOUT', ] );
return settings;} } ```
3. Import OrganizationModule
```typescript import { Module } from '@nestjs/common'; import { OrganizationModule } from '@venturialstd/organization';
@Module({ imports: [OrganizationModule], providers: [YourModuleService], }) export class YourModule {} ```
Database Schema
```sql CREATE TABLE organization_settings ( id UUID PRIMARY KEY, organizationId UUID REFERENCES organization(id) ON DELETE CASCADE, module VARCHAR NOT NULL, -- 'GITLAB', 'JIRA', 'YOUR_MODULE' section VARCHAR NOT NULL, -- 'CREDENTIALS', 'OAUTH', 'PREFERENCES' key VARCHAR NOT NULL, -- 'TOKEN', 'API_KEY', 'HOST' value VARCHAR NOT NULL, -- Actual value type settings_type_enum NOT NULL, label VARCHAR NOT NULL, description VARCHAR, options JSONB, sortOrder INTEGER DEFAULT 0, createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW(), updatedAt TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(organizationId, module, section, key) ); ```
🧪 Test Server
The module includes a standalone test server for development and testing.
Start the Test Server
```bash cd src/organization npm install npm run start:dev ```
The server runs on http://localhost:3002
Test Endpoints
```bash
Get all organizations
GET http://localhost:3002/test/organizations
Get organization by ID
GET http://localhost:3002/test/organizations/:id
Get organization settings
GET http://localhost:3002/test/organizations/:id/settings
Get settings for specific module
GET http://localhost:3002/test/organizations/:id/settings?module=GITLAB
Create/update setting
PUT http://localhost:3002/test/organizations/:id/settings Content-Type: application/json
{ "module": "GITLAB", "section": "CREDENTIALS", "key": "TOKEN", "value": "your-token", "label": "GitLab Token", "type": "secret" } ```
Test Controller
The test controller is for reference and development only. In production, implement your own controllers with:
- Custom authentication
- Custom authorization
- Custom validation
- Custom error handling
Location: `test/controllers/organization-settings-test.controller.ts`
📖 Migration Guide
Running Migrations
```bash
Generate migration
npm run migration:generate -- YourMigrationName
Run migrations
npm run migration:run
Revert migration
npm run migration:revert ```
Required Migrations
The module requires these migrations:
- Create organizations table
- Create organization_users table
- Create organization_settings table
Migrations are located in: `database/migrations/`
💻 Examples
Example 1: Create Organization with Owner
```typescript @Injectable() export class OnboardingService { constructor( private readonly orgService: OrganizationService, private readonly orgUserService: OrganizationUserService, ) {}
async onboardUser(userId: string, companyName: string) { // Create organization const org = await this.orgService.createOrganization( companyName, this.generateSlug(companyName), `${this.generateSlug(companyName)}.example.com`, `${companyName} organization`, userId );
// Owner is automatically added by createOrganization
return org;}
private generateSlug(name: string): string { return name.toLowerCase().replace(/\s+/g, '-'); } } ```
Example 2: Check User Permission
```typescript @Controller('organizations/:organizationId/data') export class DataController { constructor( private readonly orgUserService: OrganizationUserService, ) {}
@Get() @UseGuards(OrganizationGuard) async getData( @OrganizationId() organizationId: string, @UserId() userId: string, ) { const role = await this.orgUserService.getUserRole( organizationId, userId );
// Check if user has required role
if (![ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER].includes(role)) {
throw new ForbiddenException('Insufficient permissions');
}
// Return data} } ```
Example 3: Module with Organization Settings
```typescript // GitLab Service @Injectable() export class GitLabService { constructor( private readonly orgSettings: OrganizationSettingsService ) {}
async getClient(organizationId: string) { // Get org-specific GitLab credentials const [host, token] = await Promise.all([ this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:HOST'), this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:TOKEN'), ]);
// Validate configuration
if (!host || !token) {
throw new Error(
'GitLab not configured for this organization. ' +
'Please configure it in organization settings.'
);
}
// Return organization-specific client
return new GitLab({ host, token });}
async getRepositories(organizationId: string) { const client = await this.getClient(organizationId); return client.Projects.all(); } } ```
Example 4: Bulk Configure Organization
```typescript @Injectable() export class OrganizationSetupService { constructor( private readonly orgSettings: OrganizationSettingsService, ) {}
async configureGitLab( organizationId: string, config: { host: string; token: string } ) { return this.orgSettings.bulkUpdateSettings(organizationId, [ { module: 'GITLAB', section: 'CREDENTIALS', key: 'HOST', value: config.host, label: 'GitLab Host', type: SETTINGS_TYPE.TEXT, }, { module: 'GITLAB', section: 'CREDENTIALS', key: 'TOKEN', value: config.token, label: 'GitLab Token', type: SETTINGS_TYPE.SECRET, }, ]); } } ```
Example 5: Settings Resolution Pattern
```typescript @Injectable() export class IntegrationService { constructor( private readonly orgSettings: OrganizationSettingsService, ) {}
async getIntegrationConfig(organizationId: string, integration: string) { // Get all settings for this integration const settings = await this.orgSettings.getAllForOrganization( organizationId, integration );
// Check if configured
if (settings.length === 0) {
return {
configured: false,
message: \`\${integration} is not configured for this organization\`,
};
}
// Convert to key-value object
const config = settings.reduce((acc, setting) => {
acc[\`\${setting.section}:\${setting.key}\`] = setting.value;
return acc;
}, {});
return {
configured: true,
config,
};} } ```
📝 Best Practices
1. Always Check Configuration
```typescript // ❌ Don't assume settings exist const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN'); await client.connect(token); // Could be undefined!
// ✅ Always validate const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN'); if (!token) { throw new Error('Module not configured'); } await client.connect(token); ```
2. Use Bulk Operations
```typescript // ❌ Multiple single calls const host = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:HOST'); const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN'); const secret = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:SECRET');
// ✅ Single bulk call const settings = await orgSettings.getManySettings(orgId, [ 'MODULE:CREDENTIALS:HOST', 'MODULE:CREDENTIALS:TOKEN', 'MODULE:CREDENTIALS:SECRET', ]); ```
3. Implement Your Own Controllers
```typescript // ❌ Don't use test controller in production import { OrganizationSettingsTestController } from '@venturialstd/organization';
// ✅ Implement your own @Controller('api/organizations/:organizationId/settings') @UseGuards(JwtAuthGuard, OrganizationGuard) export class OrganizationSettingsController { // Your implementation with proper auth } ```
4. Use Type-Safe Settings Types
```typescript // ✅ Import and use the enum import { SETTINGS_TYPE } from '@venturialstd/organization';
const setting = { type: SETTINGS_TYPE.SECRET, // Type-safe // ... }; ```
📄 License
MIT
🤝 Contributing
Contributions welcome! Please read the contributing guidelines before submitting PRs.
