Skip to main content

Directory

Directory management functionality with S3 integration for the MBC CQRS Serverless framework.

Installation

npm install @mbc-cqrs-serverless/directory

Overview

The Directory package provides comprehensive file and folder management in a multi-tenant CQRS architecture. It integrates with Amazon S3 for file storage and supports granular access permissions.

Features

  • Directory CRUD Operations: Create, read, update, and delete folders and files
  • S3 Integration: Full file management with Amazon S3
  • Access Permissions: Granular permissions for specific folders and files
  • Multi-tenant Support: Tenant-isolated directory management
  • Event-Driven Architecture: Built on CQRS pattern with command/event handling
  • RESTful API: Complete REST API for directory operations
  • Version History: Track and restore previous versions of files and folders

Basic Setup

Module Configuration

import { DirectoryStorageModule } from '@mbc-cqrs-serverless/directory';
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
imports: [
DirectoryStorageModule.register({
enableController: true, // Enable REST API endpoints
prismaService: PrismaService, // Required when enableController is true
dataSyncHandlers: [], // Optional data sync handlers
}),
],
})
export class AppModule {}

API Endpoints

MethodEndpointDescription
POST/api/directory/Create a new file or folder
GET/api/directory/summaryGet tenant file size summary
GET/api/directory/:idGet details for a specific file or folder
GET/api/directory/:id/historyGet version history of a file or folder
POST/api/directory/:id/history/:version/restoreRestore a specific version
PUT/api/directory/:id/restoreRestore a temporarily deleted item
PATCH/api/directory/:idUpdate a specific file or folder
PATCH/api/directory/:id/permissionUpdate permissions for a file or folder
PATCH/api/directory/:id/renameRename a file or folder
PATCH/api/directory/:id/copyCopy a file or folder
PATCH/api/directory/:id/moveMove a file or folder
DELETE/api/directory/:idSoft delete a file or folder
DELETE/api/directory/:id/binPermanently delete a file and remove from S3
POST/api/directory/file/viewGenerate a presigned URL for viewing a file
POST/api/directory/fileGenerate a presigned URL for uploading a file

Creating Folders

import { DirectoryService, DirectoryCreateDto, DirectoryDataEntity } from '@mbc-cqrs-serverless/directory';
import { IInvoke } from '@mbc-cqrs-serverless/core';
import { Injectable } from '@nestjs/common';

@Injectable()
export class FolderService {
constructor(private readonly directoryService: DirectoryService) {}

async createFolder(
createDto: DirectoryCreateDto,
invokeContext: IInvoke,
): Promise<DirectoryDataEntity> {
return this.directoryService.create(createDto, { invokeContext });
}
}

Uploading Files

async uploadFile(
createDto: DirectoryCreateDto,
invokeContext: IInvoke,
): Promise<DirectoryDataEntity> {
// File upload is handled through the create method with file content
return this.directoryService.create(createDto, { invokeContext });
}

Listing Contents

async getDirectory(
detailDto: DetailDto,
invokeContext: IInvoke,
queryDto: DirectoryDetailDto,
): Promise<DirectoryDataEntity> {
return this.directoryService.findOne(detailDto, { invokeContext }, queryDto);
}

async getDirectoryHistory(
detailDto: DetailDto,
invokeContext: IInvoke,
queryDto: DirectoryDetailDto,
): Promise<DirectoryDataListEntity> {
return this.directoryService.findHistory(detailDto, { invokeContext }, queryDto);
}

File Operations

// Get file attributes
async getFileAttributes(detailDto: DetailDto): Promise<DirectoryAttributes> {
return this.directoryService.getItemAttributes(detailDto);
}

// Get file item
async getFile(detailDto: DetailDto): Promise<DirectoryDataEntity> {
return this.directoryService.getItem(detailDto);
}

// Soft delete (marks as deleted)
async removeItem(
detailDto: DetailDto,
invokeContext: IInvoke,
queryDto: DirectoryDetailDto,
): Promise<DirectoryDataEntity> {
return this.directoryService.remove(detailDto, { invokeContext }, queryDto);
}

// Permanently remove file and delete from S3
async removeFile(
detailDto: DetailDto,
invokeContext: IInvoke,
queryDto: DirectoryDetailDto,
): Promise<DirectoryDataEntity> {
return this.directoryService.removeFile(detailDto, { invokeContext }, queryDto);
}

Updating Items

import { DirectoryUpdateDto } from '@mbc-cqrs-serverless/directory';

async updateItem(
detailDto: DetailDto,
updateDto: DirectoryUpdateDto,
invokeContext: IInvoke,
): Promise<DirectoryDataEntity> {
return this.directoryService.update(detailDto, updateDto, { invokeContext });
}

Renaming Items

import { DirectoryRenameDto } from '@mbc-cqrs-serverless/directory';

async renameItem(
detailDto: DetailDto,
renameDto: DirectoryRenameDto,
invokeContext: IInvoke,
): Promise<DirectoryDataEntity> {
return this.directoryService.rename(detailDto, renameDto, { invokeContext });
}

Managing Permissions

Permission Types

The directory package supports different permission types:

enum FilePermission {
GENERAL = 'GENERAL', // General access for everyone
RESTRICTED = 'RESTRICTED', // Restricted to specific users
DOMAIN = 'DOMAIN', // Restricted to specific email domain
TENANT = 'TENANT', // Restricted to tenant members
}

enum FileRole {
READ = 'READ',
WRITE = 'WRITE',
DELETE = 'DELETE',
CHANGE_PERMISSION = 'CHANGE_PERMISSION',
TAKE_OWNERSHIP = 'TAKE_OWNERSHIP',
}

enum EmailType {
EMAIL = 'EMAIL', // Individual email address
EMAIL_GROUP = 'EMAIL_GROUP', // Email group or distribution list
}

Directory Attributes

The DirectoryAttributes interface defines the metadata for files and folders:

interface DirectoryAttributes {
expirationTime?: string; // Expiration time for the item
fileSize?: number; // File size in bytes
fileType?: string; // MIME type of the file
parentId?: string; // Parent folder ID
owner: OwnerDto; // Owner information
s3Key?: string; // S3 object key
ancestors?: string[]; // Array of ancestor folder IDs
inheritance?: boolean; // Whether to inherit parent permissions
tags?: string[]; // Tags for categorization
permission?: PermissionDto; // Permission settings
}

interface OwnerDto {
email: string; // Owner's email address
ownerId: string; // Owner's user ID
}

interface PermissionDto {
type: FilePermission; // Permission type
role: FileRole; // Default role for this permission
domain?: DomainDto; // Domain restriction (for DOMAIN type)
users?: UserPermissionDto[]; // User-specific permissions (for RESTRICTED type)
}

interface DomainDto {
email: string; // Email domain (e.g., "example.com")
}

interface UserPermissionDto {
email: string; // User's email address
role: FileRole; // Role assigned to this user
id: string; // User ID
type: EmailType; // Email type (EMAIL or EMAIL_GROUP)
}

Updating Permissions

import { DirectoryUpdatePermissionDto } from '@mbc-cqrs-serverless/directory';

async updatePermission(
detailDto: DetailDto,
updateDto: DirectoryUpdatePermissionDto,
invokeContext: IInvoke,
): Promise<DirectoryDataEntity> {
return this.directoryService.updatePermission(detailDto, updateDto, { invokeContext });
}

Checking Permissions

async hasPermission(
itemId: DetailDto,
requiredRole: FileRole[],
user?: { email?: string; tenant?: string },
): Promise<boolean> {
return this.directoryService.hasPermission(itemId, requiredRole, user);
}

async getEffectiveRole(
itemId: DetailDto,
user?: { email?: string; tenant?: string },
): Promise<FileRole | null> {
return this.directoryService.getEffectiveRole(itemId, user);
}

Moving and Copying

Move Item

import { DirectoryMoveDto } from '@mbc-cqrs-serverless/directory';

async moveItem(
detailDto: DetailDto,
moveDto: DirectoryMoveDto,
invokeContext: IInvoke,
): Promise<DirectoryDataEntity> {
return this.directoryService.move(detailDto, moveDto, { invokeContext });
}

Copy Item

import { DirectoryCopyDto } from '@mbc-cqrs-serverless/directory';

async copyItem(
detailDto: DetailDto,
copyDto: DirectoryCopyDto,
invokeContext: IInvoke,
): Promise<DirectoryDataEntity> {
return this.directoryService.copy(detailDto, copyDto, { invokeContext });
}

Version History

Restore Previous Version

async restoreVersion(
detailDto: DetailDto,
version: string,
queryDto: DirectoryDetailDto,
invokeContext: IInvoke,
): Promise<DirectoryDataEntity> {
return this.directoryService.restoreHistoryItem(detailDto, version, queryDto, { invokeContext });
}

Restore Temporarily Deleted Item

async restoreTemporary(
detailDto: DetailDto,
queryDto: DirectoryDetailDto,
invokeContext: IInvoke,
): Promise<DirectoryDataEntity> {
return this.directoryService.restoreTemporary(detailDto, queryDto, { invokeContext });
}

Directory DTOs

The directory package provides several DTOs for different operations:

DirectoryCreateDto

interface DirectoryCreateDto {
name: string; // Item name
type: string; // Item type (e.g., 'folder', 'file')
attributes?: DirectoryAttributes; // Optional attributes
}

DirectoryUpdateDto

interface DirectoryUpdateDto {
email: string; // Requester's email for permission check
name?: string; // New name (optional)
isDeleted?: boolean; // Deletion flag
attributes?: DirectoryAttributes; // Updated attributes
}

DirectoryRenameDto

interface DirectoryRenameDto {
name: string; // New name
email: string; // Requester's email for permission check
}

DirectoryMoveDto

interface DirectoryMoveDto {
parentId?: string; // Target parent folder ID
email: string; // Requester's email for permission check
}

DirectoryCopyDto

interface DirectoryCopyDto {
path: string; // S3 path for the copied file
parentId?: string; // Target parent folder ID
email: string; // Requester's email for permission check
}

DirectoryDetailDto

interface DirectoryDetailDto {
email: string; // Requester's email for permission check
}

DirectoryUpdatePermissionDto

interface DirectoryUpdatePermissionDto {
email: string; // Requester's email for permission check
attributes?: {
permission?: PermissionDto; // New permission settings
inheritance?: boolean; // Whether to inherit parent permissions
};
}

Directory Structure

Example directory structure:

/
├── documents/
│ ├── reports/
│ │ ├── 2024-Q1-report.pdf
│ │ └── 2024-Q2-report.pdf
│ └── contracts/
│ └── contract-001.pdf
├── images/
│ ├── logo.png
│ └── banner.jpg
└── templates/
└── invoice-template.docx

Multi-tenant Isolation

Directories are automatically isolated by tenant through the invoke context:

@Controller('api/directory')
export class DirectoryController {
constructor(private readonly directoryService: DirectoryService) {}

@Get(':id')
async findOne(
@INVOKE_CONTEXT() invokeContext: IInvoke,
@DetailKeys() detailDto: DetailDto,
@Query() queryDto: DirectoryDetailDto,
): Promise<DirectoryDataEntity> {
// Tenant isolation is handled through the pk structure
return this.directoryService.findOne(detailDto, { invokeContext }, queryDto);
}
}

Event Handling

Handle directory data synchronization using data sync handlers:

import { IDataSyncHandler, DataEntity } from '@mbc-cqrs-serverless/core';

export class DirectoryDataSyncHandler implements IDataSyncHandler {
async onCreated(data: DataEntity): Promise<void> {
console.log('Directory created:', data.name);
// Sync to RDS, notify users, update indexes, etc.
}

async onUpdated(data: DataEntity): Promise<void> {
console.log('Directory updated:', data.name);
}

async onDeleted(data: DataEntity): Promise<void> {
console.log('Directory deleted:', data.name);
}
}

Best Practices

  1. Use Folders for Organization: Create a logical folder structure for easy navigation
  2. Set Permissions Early: Configure permissions when creating directories
  3. Handle Large Files: For large files, use presigned URLs for direct S3 upload
  4. Clean Up: Implement retention policies for temporary files
  5. Audit Trail: Use events to maintain an audit trail of all operations
  6. Use Soft Delete: Prefer soft delete (remove) over permanent delete (removeFile) for data recovery