Skip to main content
Sonamu provides multiple methods to access the Context. These methods are useful when you need Context outside of API methods.

Sonamu.getContext()

Returns the Context of the currently executing request. Managed through AsyncLocalStorage, so it can be called anywhere within the same request stack.

Signature

Sonamu.getContext(): Context

Return Value

  • Context: The Context object of the current request
  • Test Environment: Empty Context object (request and reply are null)

Exceptions

  • Error: When Context cannot be found (production environment only)

Usage Examples

Using in Helper Functions

// src/utils/auth-helper.ts
import { Sonamu } from "sonamu";

export function getCurrentUser() {
  const ctx = Sonamu.getContext();
  return ctx.user;
}

export function requireAuth() {
  const user = getCurrentUser();
  if (!user) {
    throw new UnauthorizedException("Authentication required");
  }
  return user;
}
// src/models/post.model.ts
import { requireAuth } from "../utils/auth-helper";

class PostModelClass extends BaseModel {
  @api()
  async createPost(title: string, content: string) {
    // Access user information through helper function without receiving Context directly
    const user = requireAuth();

    return this.create({
      title,
      content,
      authorId: user.id
    });
  }
}

Using in Service Layer

// src/services/notification.service.ts
import { Sonamu } from "sonamu";

export class NotificationService {
  async sendNotification(userId: number, message: string) {
    const ctx = Sonamu.getContext();

    // Send localized message using Context's locale information
    const localizedMessage = this.localize(message, ctx.locale);

    // Notification sending logic
    await this.send(userId, localizedMessage);
  }

  private localize(message: string, locale?: string) {
    // Localization logic
    return message;
  }
}

Test Environment Behavior

In test environments, returns an empty Context without throwing errors even when Context is not injected:
// test/post.test.ts
import { Sonamu } from "sonamu";

describe("PostModel", () => {
  it("should create post", async () => {
    // Works in test environment without Context
    const ctx = Sonamu.getContext();
    console.log(ctx.request); // null (no error)
  });
});
For tests that need actual Context, use runWithMockContext:
import { runWithMockContext } from "sonamu/test";

it("should use context in test", async () => {
  await runWithMockContext(
    {
      user: { id: 1, email: "test@example.com" }
    },
    async () => {
      const ctx = Sonamu.getContext();
      expect(ctx.user?.id).toBe(1);
    }
  );
});

File Upload Context

For file upload requests, uploaded file information is accessed through Sonamu.getContext(). Available only in methods with the @upload decorator. Two properties are used depending on the upload mode:
  • Buffer mode (default): bufferedFiles - files loaded in memory, suitable for MD5 calculation/image processing
  • Stream mode: uploadedFiles - files streamed directly to storage, suitable for large files

Signature

// Buffer mode (default)
const { bufferedFiles } = Sonamu.getContext();
// bufferedFiles: BufferedFile[]

// Stream mode
const { uploadedFiles } = Sonamu.getContext();
// uploadedFiles: UploadedFile[]

Type

// File upload properties of Context
bufferedFiles?: BufferedFile[];  // buffer mode
uploadedFiles?: UploadedFile[];  // stream mode

Notes

  • File properties are only set when using the @upload decorator
  • In buffer mode (default), use bufferedFiles; in stream mode, use uploadedFiles
  • For single file uploads, access via bufferedFiles?.[0] or uploadedFiles?.[0]

Usage Examples

Single File Upload (Buffer Mode)

class FileModelClass extends BaseModel {
  @upload()
  async uploadAvatar(ctx: Context) {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0]; // Use first file

    if (!file) {
      throw new BadRequestException("File is required");
    }

    // Check file information
    console.log(file.filename);    // "avatar.jpg"
    console.log(file.mimetype);    // "image/jpeg"
    console.log(file.size);        // 102400 (bytes)
    console.log(file.extname);     // "jpg"

    // Save file (diskName, key order)
    const key = `avatars/${Date.now()}_${file.filename}`;
    const url = await file.saveToDisk("fs", key);

    return { url, filename: file.filename, size: file.size };
  }
}

Multiple File Upload (Buffer Mode)

class FileModelClass extends BaseModel {
  @upload()
  async uploadDocuments(ctx: Context, folderId: number) {
    const { bufferedFiles } = Sonamu.getContext();

    if (!bufferedFiles || bufferedFiles.length === 0) {
      throw new BadRequestException("At least one file is required");
    }

    const savedFiles = await Promise.all(
      bufferedFiles.map(async (file, i) => {
        const key = `documents/${Date.now()}_${i}_${file.filename}`;
        const url = await file.saveToDisk("fs", key);
        return { url, filename: file.filename, size: file.size };
      })
    );

    // Save file metadata to database
    await this.createMany(
      savedFiles.map((file) => ({
        folderId,
        filename: file.filename,
        url: file.url,
        size: file.size
      }))
    );

    return { count: savedFiles.length, files: savedFiles };
  }
}

Large File Upload (Stream Mode)

class FileModelClass extends BaseModel {
  @upload({ mode: "stream" })
  async uploadLargeFiles(ctx: Context) {
    const { uploadedFiles } = Sonamu.getContext();

    if (!uploadedFiles || uploadedFiles.length === 0) {
      throw new BadRequestException("File is required");
    }

    // Already uploaded to storage
    return {
      files: uploadedFiles.map((file) => ({
        filename: file.filename,
        url: file.url,
        size: file.size,
      })),
    };
  }
}

Using in Helper Functions

// src/utils/file-helper.ts
import { Sonamu } from "sonamu";

export function getBufferedFiles() {
  const { bufferedFiles } = Sonamu.getContext();
  return bufferedFiles;
}

export function validateFileTypes(allowedTypes: string[]) {
  const { bufferedFiles } = Sonamu.getContext();

  if (!bufferedFiles) return;

  for (const file of bufferedFiles) {
    if (!allowedTypes.includes(file.mimetype)) {
      throw new BadRequestException(
        `File type not allowed: ${file.mimetype}`
      );
    }
  }
}
class FileModelClass extends BaseModel {
  @api()
  @upload()
  async uploadImages(ctx: Context) {
    // Allow only image files
    validateFileTypes([
      "image/jpeg",
      "image/png",
      "image/gif",
      "image/webp"
    ]);

    const files = getBufferedFiles();

    // File processing logic
    // ...
  }
}

BufferedFile (Buffer Mode)

class BufferedFile {
  /** Original filename */
  get filename(): string;

  /** MIME type */
  get mimetype(): string;

  /** File size (bytes) */
  get size(): number;

  /** Extension (without dot) */
  get extname(): string | false;

  /** File Buffer (getter) */
  get buffer(): Buffer;

  /** Access to original MultipartFile */
  get raw(): MultipartFile;

  /** Calculate MD5 hash */
  async md5(): Promise<string>;

  /**
   * Save file to disk
   * @param diskName Disk name (e.g., 'fs', 's3')
   * @param key Storage path (e.g., 'uploads/avatar.png')
   * @returns URL of saved file
   */
  async saveToDisk(diskName: DriverKey, key: string): Promise<string>;
}

UploadedFile (Stream Mode)

class UploadedFile {
  /** Original filename */
  get filename(): string;

  /** MIME type */
  get mimetype(): string;

  /** File size (bytes) */
  get size(): number;

  /** Extension (without dot) */
  get extname(): string | false;

  /** URL of the stored file (Unsigned) */
  get url(): string;

  /** URL of the stored file (Signed, with expiration) */
  get signedUrl(): string;

  /** Key in storage */
  get key(): string;

  /** Storage disk name */
  get diskName(): DriverKey;

  /** Download file from storage */
  async download(): Promise<Buffer>;
}

Precautions

AsyncLocalStorage Stack

getContext() works through AsyncLocalStorage, so it must be called within the same asynchronous execution stack:
// ✅ Correct usage
class PostModelClass extends BaseModel {
  @api()
  async createPost(title: string) {
    const user = this.getCurrentUser(); // Within same stack
    return this.create({ title, authorId: user.id });
  }

  private getCurrentUser() {
    return Sonamu.getContext().user;
  }
}

// ❌ Incorrect usage
class PostModelClass extends BaseModel {
  @api()
  async createPost(title: string) {
    // setTimeout creates a new execution stack
    setTimeout(() => {
      const ctx = Sonamu.getContext(); // Error!
    }, 1000);
  }
}

@upload Decorator Required

The bufferedFiles/uploadedFiles properties can only be used in methods with the @upload decorator:
// ✅ Correct usage (Buffer Mode)
@api()
@upload()
async uploadFile() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0]; // OK
}

// ✅ Correct usage (Stream Mode)
@api()
@upload({ mode: "stream" })
async uploadFile() {
  const { uploadedFiles } = Sonamu.getContext();
  const file = uploadedFiles?.[0]; // OK
}

// ❌ Incorrect usage
@api()
async uploadFile() {
  const { bufferedFiles } = Sonamu.getContext();
  // bufferedFiles is undefined
}