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

hasPermission() returns true when the user holds at least one of the required roles on the item. getEffectiveRole() returns the highest role the user holds, or null if they have no access.

// Returns true if user holds any of the required roles on the item
async hasPermission(
itemId: DetailDto,
requiredRole: FileRole[],
user?: { email?: string; tenant?: string },
): Promise<boolean> {
return this.directoryService.hasPermission(itemId, requiredRole, user);
}

// Returns the effective FileRole the user holds, or null if no access
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 { CommandModel, IDataSyncHandler } from '@mbc-cqrs-serverless/core';
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma';

@Injectable()
export class DirectoryDataSyncHandler implements IDataSyncHandler {
constructor(private readonly prisma: PrismaService) {}

async up(cmd: CommandModel): Promise<any> {
// Called automatically on every command event — sync to RDS, notify users, update indexes, etc.
await this.prisma.directory.upsert({
where: { sk: cmd.sk },
create: { sk: cmd.sk, pk: cmd.pk, name: cmd.name, ...cmd.attributes },
update: { name: cmd.name, ...cmd.attributes },
});
}

async down(cmd: CommandModel): Promise<any> {
// Reserved for manual rollback — not called automatically by the framework
await this.prisma.directory.delete({ where: { sk: cmd.sk } });
}
}

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