Sonamu.getContext() within API methods.
Basic Usage
Getting Context
import { BaseModelClass, api, Sonamu } from "sonamu";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
}
@api({ httpMethod: "GET" })
async getCurrentUser(): Promise<User> {
// Get Context
const context = Sonamu.getContext();
// Access user info
if (!context.user) {
throw new Error("Authentication required");
}
const rdb = this.getPuri("r");
const user = await rdb.table("users").where("id", context.user.id).first();
if (!user) {
throw new Error("User not found");
}
return user;
}
}
Sonamu.getContext() returns the Context of the currently executing API request. It returns the
correct Context even during async operations.When to Use?
1. Check Authenticated User
class PostModelClass extends BaseModelClass<
PostSubsetKey,
PostSubsetMapping,
typeof postSubsetQueries,
typeof postLoaderQueries
> {
constructor() {
super("Post", postSubsetQueries, postLoaderQueries);
}
@api({ httpMethod: "POST" })
async create(params: PostSaveParams): Promise<{ postId: number }> {
const context = Sonamu.getContext();
// Check login
if (!context.user) {
throw new Error("Login required");
}
// Set current user as author
const wdb = this.getPuri("w");
const [post] = await wdb
.table("posts")
.insert({
...params,
user_id: context.user.id,
author: context.user.username,
})
.returning({ id: "id" });
return { postId: post.id };
}
}
2. Check Permissions
class AdminModelClass extends BaseModelClass<
AdminSubsetKey,
AdminSubsetMapping,
typeof adminSubsetQueries,
typeof adminLoaderQueries
> {
constructor() {
super("Admin", adminSubsetQueries, adminLoaderQueries);
}
@api({ httpMethod: "DELETE" })
async deleteUser(userId: number): Promise<void> {
const context = Sonamu.getContext();
// Check authentication
if (!context.user) {
throw new Error("Authentication required");
}
// Check permission
if (context.user.role !== "admin") {
throw new Error("Admin permission required");
}
const wdb = this.getPuri("w");
await wdb.table("users").where("id", userId).delete();
}
}
3. Check Ownership
class PostModelClass extends BaseModelClass<
PostSubsetKey,
PostSubsetMapping,
typeof postSubsetQueries,
typeof postLoaderQueries
> {
constructor() {
super("Post", postSubsetQueries, postLoaderQueries);
}
@api({ httpMethod: "PUT" })
async update(postId: number, params: PostSaveParams): Promise<void> {
const context = Sonamu.getContext();
if (!context.user) {
throw new Error("Authentication required");
}
const rdb = this.getPuri("r");
// Get post
const post = await rdb.table("posts").where("id", postId).first();
if (!post) {
throw new Error("Post not found");
}
// Check ownership (author or admin only)
const isOwner = post.user_id === context.user.id;
const isAdmin = context.user.role === "admin";
if (!isOwner && !isAdmin) {
throw new Error("Permission denied");
}
// Update
const wdb = this.getPuri("w");
await wdb.table("posts").where("id", postId).update(params);
}
}
4. Access Request Info
class AnalyticsModelClass extends BaseModelClass<
AnalyticsSubsetKey,
AnalyticsSubsetMapping,
typeof analyticsSubsetQueries,
typeof analyticsLoaderQueries
> {
constructor() {
super("Analytics", analyticsSubsetQueries, analyticsLoaderQueries);
}
@api({ httpMethod: "POST" })
async trackEvent(params: EventParams): Promise<void> {
const context = Sonamu.getContext();
const wdb = this.getPuri("w");
// Record Request info
await wdb.table("analytics").insert({
...params,
user_id: context.user?.id,
ip_address: context.request.ip,
user_agent: context.request.headers["user-agent"],
referer: context.request.headers.referer,
timestamp: new Date(),
});
}
}
5. Set Response Headers
class FileModelClass extends BaseModelClass<
FileSubsetKey,
FileSubsetMapping,
typeof fileSubsetQueries,
typeof fileLoaderQueries
> {
constructor() {
super("File", fileSubsetQueries, fileLoaderQueries);
}
@api({ httpMethod: "GET" })
async download(fileId: number): Promise<Buffer> {
const context = Sonamu.getContext();
const rdb = this.getPuri("r");
const file = await rdb.table("files").where("id", fileId).first();
if (!file) {
throw new Error("File not found");
}
// Set download headers
context.reply.header("Content-Type", file.mime_type);
context.reply.header("Content-Disposition", `attachment; filename="${file.filename}"`);
return Buffer.from(file.content);
}
}
Helper Function Patterns
Reusable Auth Check
class BaseModelClass {
// Auth check helper
protected requireAuth(): ContextUser {
const context = Sonamu.getContext();
if (!context.user) {
context.reply.status(401);
throw new Error("Authentication required");
}
return context.user;
}
// Role check helper
protected requireRole(role: UserRole): ContextUser {
const user = this.requireAuth();
if (user.role !== role) {
const context = Sonamu.getContext();
context.reply.status(403);
throw new Error(`${role} permission required`);
}
return user;
}
// Admin check helper
protected requireAdmin(): ContextUser {
return this.requireRole("admin");
}
}
// Usage
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
}
@api({ httpMethod: "DELETE" })
async remove(userId: number): Promise<void> {
// Simple permission check
this.requireAdmin();
const wdb = this.getPuri("w");
await wdb.table("users").where("id", userId).delete();
}
@api({ httpMethod: "GET" })
async getMyProfile(): Promise<User> {
// Just check login
const user = this.requireAuth();
const rdb = this.getPuri("r");
return rdb.table("users").where("id", user.id).first();
}
}
Ownership Check Helper
class BaseModelClass {
protected async requireOwnership(
tableName: string,
itemId: number,
userIdField: string = "user_id",
): Promise<void> {
const user = this.requireAuth();
const context = Sonamu.getContext();
const rdb = this.getPuri("r");
const item = await rdb.table(tableName).where("id", itemId).first();
if (!item) {
context.reply.status(404);
throw new Error("Item not found");
}
// Allow owner or admin only
const isOwner = item[userIdField] === user.id;
const isAdmin = user.role === "admin";
if (!isOwner && !isAdmin) {
context.reply.status(403);
throw new Error("Permission denied");
}
}
}
// Usage
class PostModelClass extends BaseModelClass<
PostSubsetKey,
PostSubsetMapping,
typeof postSubsetQueries,
typeof postLoaderQueries
> {
constructor() {
super("Post", postSubsetQueries, postLoaderQueries);
}
@api({ httpMethod: "DELETE" })
async remove(postId: number): Promise<void> {
// Auto ownership check
await this.requireOwnership("posts", postId);
const wdb = this.getPuri("w");
await wdb.table("posts").where("id", postId).delete();
}
}
Usage in Multiple Locations
In Model Methods
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
}
@api({ httpMethod: "GET" })
async getCurrentUser(): Promise<User> {
const context = Sonamu.getContext();
if (!context.user) {
throw new Error("Authentication required");
}
return this.getUserById(context.user.id);
}
// Also available in private methods
private async getUserById(userId: number): Promise<User> {
const context = Sonamu.getContext();
// Request logging
console.log("getUserById called from IP:", context.request.ip);
const rdb = this.getPuri("r");
const user = await rdb.table("users").where("id", userId).first();
if (!user) {
throw new Error("User not found");
}
return user;
}
}
In Frames
class AnalyticsFrame extends BaseFrameClass {
frameName = "Analytics";
@api({ httpMethod: "POST" })
async track(params: TrackParams): Promise<void> {
const context = Sonamu.getContext();
const wdb = this.getPuri("w");
await wdb.table("analytics").insert({
event_type: params.eventType,
event_data: JSON.stringify(params.data),
user_id: context.user?.id,
ip_address: context.request.ip,
user_agent: context.request.headers["user-agent"],
timestamp: new Date(),
});
}
}
In Async Operations
class NotificationModelClass extends BaseModelClass<
NotificationSubsetKey,
NotificationSubsetMapping,
typeof notificationSubsetQueries,
typeof notificationLoaderQueries
> {
constructor() {
super("Notification", notificationSubsetQueries, notificationLoaderQueries);
}
@api({ httpMethod: "POST" })
async sendBulk(params: { userIds: number[]; message: string }): Promise<void> {
const context = Sonamu.getContext();
// Context accessible in async operations
const promises = params.userIds.map(async (userId) => {
const innerContext = Sonamu.getContext();
// Same Context
console.log(innerContext.user?.id === context.user?.id); // true
return this.sendNotification(userId, params.message);
});
await Promise.all(promises);
}
private async sendNotification(userId: number, message: string): Promise<void> {
const context = Sonamu.getContext();
// Same Context here too
const wdb = this.getPuri("w");
await wdb.table("notifications").insert({
user_id: userId,
message,
sender_id: context.user?.id,
created_at: new Date(),
});
}
}
Practical Patterns
Audit Log
class BaseModelClass {
protected async logAudit(action: string, details: any): Promise<void> {
const context = Sonamu.getContext();
const wdb = this.getPuri("w");
await wdb.table("audit_logs").insert({
user_id: context.user?.id,
action,
details: JSON.stringify(details),
ip_address: context.request.ip,
user_agent: context.request.headers["user-agent"],
timestamp: new Date(),
});
}
}
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
}
@api({ httpMethod: "DELETE" })
async remove(userId: number): Promise<void> {
this.requireAdmin();
const rdb = this.getPuri("r");
const user = await rdb.table("users").where("id", userId).first();
// Record audit log
await this.logAudit("USER_DELETE", {
targetUserId: userId,
targetUsername: user.username,
});
const wdb = this.getPuri("w");
await wdb.table("users").where("id", userId).delete();
}
}
Rate Limit Check
class ApiModelClass extends BaseModelClass<
ApiSubsetKey,
ApiSubsetMapping,
typeof apiSubsetQueries,
typeof apiLoaderQueries
> {
constructor() {
super("Api", apiSubsetQueries, apiLoaderQueries);
}
private async checkRateLimit(): Promise<void> {
const context = Sonamu.getContext();
const rdb = this.getPuri("r");
const key = context.user ? `user:${context.user.id}` : `ip:${context.request.ip}`;
const oneMinuteAgo = new Date(Date.now() - 60000);
const [{ count }] = await rdb
.table("request_logs")
.where("key", key)
.where("timestamp", ">=", oneMinuteAgo)
.count({ count: "*" });
if (count >= 100) {
context.reply.status(429);
throw new Error("Too many requests");
}
// Log request
const wdb = this.getPuri("w");
await wdb.table("request_logs").insert({
key,
timestamp: new Date(),
});
}
@api({ httpMethod: "GET" })
async list(): Promise<any[]> {
await this.checkRateLimit();
const rdb = this.getPuri("r");
return rdb.table("items").select("*");
}
}
Internationalization Support
class I18nModelClass extends BaseModelClass<
I18nSubsetKey,
I18nSubsetMapping,
typeof i18nSubsetQueries,
typeof i18nLoaderQueries
> {
constructor() {
super("I18n", i18nSubsetQueries, i18nLoaderQueries);
}
private getLanguage(): string {
const context = Sonamu.getContext();
// Extract language from Accept-Language header
const acceptLanguage = context.request.headers["accept-language"];
if (acceptLanguage) {
const lang = acceptLanguage.split(",")[0].split("-")[0];
return lang;
}
return "en"; // Default
}
@api({ httpMethod: "GET" })
async getMessages(): Promise<Record<string, string>> {
const language = this.getLanguage();
const rdb = this.getPuri("r");
const messages = await rdb.table("translations").where("language", language).select("*");
return messages.reduce(
(acc, msg) => {
acc[msg.key] = msg.value;
return acc;
},
{} as Record<string, string>,
);
}
}
Cautions
Cautions when using Context: 1. Can only be called within API methods 2. Cannot be called in
constructors or class fields 3. Same Context maintained across async boundaries 4. Multiple calls
return the same object 5. Recommended to treat Context as read-only
Common Mistakes
// ❌ Wrong: Calling in constructor
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
private currentUser?: ContextUser;
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
// No Context in constructor
const context = Sonamu.getContext(); // ← Error
this.currentUser = context.user;
}
}
// ❌ Wrong: Calling in class field
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
// No Context at class field initialization
private context = Sonamu.getContext(); // ← Error
}
// ❌ Wrong: Calling outside API method
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
private getContext() {
// Private methods are okay, but
// calling outside API request context causes error
return Sonamu.getContext();
}
}
// ✅ Correct: Calling within API method
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
}
@api({ httpMethod: "GET" })
async getCurrentUser(): Promise<User> {
const context = Sonamu.getContext(); // ← OK
// ...
}
private async helperMethod(): Promise<void> {
// OK when called from API method
const context = Sonamu.getContext(); // ← OK
// ...
}
}
Next Steps
SonamuContext
Understanding Context structure
Custom Context
Extending Context
@api Decorator
API basic usage
Error Handling
API error handling