Sonamu.getContext()λ₯Ό νΈμΆνμ¬ νμ¬ μμ²μ Contextλ₯Ό κ°μ Έμ¬ μ μμ΅λλ€.
κΈ°λ³Έ μ¬μ©λ²
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> {
// Context κ°μ Έμ€κΈ°
const context = Sonamu.getContext();
// user μ 보 μ κ·Ό
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()λ νμ¬ μ€ν μ€μΈ API μμ²μ Contextλ₯Ό λ°νν©λλ€. λΉλκΈ° μμ
μ€μλ μ¬λ°λ₯Έ
Contextλ₯Ό λ°νν©λλ€.μΈμ μ¬μ©νλ?
1. μΈμ¦λ μ¬μ©μ νμΈ
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();
// λ‘κ·ΈμΈ νμΈ
if (!context.user) {
throw new Error("Login required");
}
// μμ±μλ‘ νμ¬ μ¬μ©μ μ€μ
const wdb = this.getPuri("w");
const [post] = await wdb
.table("posts")
.insert({
...params,
user_id: context.user.id,
author: context.user.username,
})
.returning("id");
return { postId: post.id };
}
}
2. κΆν νμΈ
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();
// μΈμ¦ νμΈ
if (!context.user) {
throw new Error("Authentication required");
}
// κΆν νμΈ
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. μμ κΆ νμΈ
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");
// κ²μκΈ μ‘°ν
const post = await rdb.table("posts").where("id", postId).first();
if (!post) {
throw new Error("Post not found");
}
// μμ κΆ νμΈ (μμ±μμ΄κ±°λ κ΄λ¦¬μλ§)
const isOwner = post.user_id === context.user.id;
const isAdmin = context.user.role === "admin";
if (!isOwner && !isAdmin) {
throw new Error("Permission denied");
}
// μμ
const wdb = this.getPuri("w");
await wdb.table("posts").where("id", postId).update(params);
}
}
4. Request μ 보 μ κ·Ό
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");
// Request μ 보 κΈ°λ‘
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. Response ν€λ μ€μ
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");
}
// λ€μ΄λ‘λ ν€λ μ€μ
context.reply.header("Content-Type", file.mime_type);
context.reply.header("Content-Disposition", `attachment; filename="${file.filename}"`);
return Buffer.from(file.content);
}
}
ν¬νΌ ν¨μ ν¨ν΄
μ¬μ¬μ© κ°λ₯ν μΈμ¦ 체ν¬
class BaseModelClass {
// μΈμ¦ νμΈ ν¬νΌ
protected requireAuth(): ContextUser {
const context = Sonamu.getContext();
if (!context.user) {
context.reply.status(401);
throw new Error("Authentication required");
}
return context.user;
}
// κΆν νμΈ ν¬νΌ
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;
}
// κ΄λ¦¬μ νμΈ ν¬νΌ
protected requireAdmin(): ContextUser {
return this.requireRole("admin");
}
}
// μ¬μ©
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 wdb = this.getPuri("w");
await wdb.table("users").where("id", userId).delete();
}
@api({ httpMethod: "GET" })
async getMyProfile(): Promise<User> {
// λ‘κ·ΈμΈλ§ νμΈ
const user = this.requireAuth();
const rdb = this.getPuri("r");
return rdb.table("users").where("id", user.id).first();
}
}
μμ κΆ νμΈ ν¬νΌ
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");
}
// μμ μμ΄κ±°λ κ΄λ¦¬μλ§ νμ©
const isOwner = item[userIdField] === user.id;
const isAdmin = user.role === "admin";
if (!isOwner && !isAdmin) {
context.reply.status(403);
throw new Error("Permission denied");
}
}
}
// μ¬μ©
class PostModelClass extends BaseModelClass<
PostSubsetKey,
PostSubsetMapping,
typeof postSubsetQueries,
typeof postLoaderQueries
> {
constructor() {
super("Post", postSubsetQueries, postLoaderQueries);
}
@api({ httpMethod: "DELETE" })
async remove(postId: number): Promise<void> {
// μμ κΆ μλ νμΈ
await this.requireOwnership("posts", postId);
const wdb = this.getPuri("w");
await wdb.table("posts").where("id", postId).delete();
}
}
μ¬λ¬ μμΉμμ μ¬μ©
Model λ©μλμμ
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);
}
// private λ©μλμμλ μ¬μ© κ°λ₯
private async getUserById(userId: number): Promise<User> {
const context = Sonamu.getContext();
// μμ² λ‘κΉ
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;
}
}
Frameμμ
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(),
});
}
}
λΉλκΈ° μμ μμ
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 μ κ·Ό κ°λ₯
const promises = params.userIds.map(async (userId) => {
const innerContext = Sonamu.getContext();
// λμΌν 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();
// μ¬κΈ°μλ λμΌν Context
const wdb = this.getPuri("w");
await wdb.table("notifications").insert({
user_id: userId,
message,
sender_id: context.user?.id,
created_at: new Date(),
});
}
}
μ€μ ν¨ν΄
κ°μ¬ λ‘κ·Έ (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();
// κ°μ¬ λ‘κ·Έ κΈ°λ‘
await this.logAudit("USER_DELETE", {
targetUserId: userId,
targetUsername: user.username,
});
const wdb = this.getPuri("w");
await wdb.table("users").where("id", userId).delete();
}
}
μμ² μλ μ ν 체ν¬
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");
}
// μμ² κΈ°λ‘
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("*");
}
}
λ€κ΅μ΄ μ§μ
class I18nModelClass extends BaseModelClass<
I18nSubsetKey,
I18nSubsetMapping,
typeof i18nSubsetQueries,
typeof i18nLoaderQueries
> {
constructor() {
super("I18n", i18nSubsetQueries, i18nLoaderQueries);
}
private getLanguage(): string {
const context = Sonamu.getContext();
// Accept-Language ν€λμμ μΈμ΄ μΆμΆ
const acceptLanguage = context.request.headers["accept-language"];
if (acceptLanguage) {
const lang = acceptLanguage.split(",")[0].split("-")[0];
return lang;
}
return "en"; // κΈ°λ³Έκ°
}
@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>,
);
}
}
μ£Όμμ¬ν
Context μ¬μ© μ μ£Όμμ¬ν: 1. API λ©μλ λ΄μμλ§ νΈμΆ κ°λ₯ 2. μμ±μλ ν΄λμ€ νλμμ νΈμΆ
λΆκ° 3. λΉλκΈ° κ²½κ³λ₯Ό λμ΄λ κ°μ Context μ μ§ 4. μ¬λ¬ λ² νΈμΆν΄λ κ°μ κ°μ²΄ λ°ν 5. Contextλ
μ½κΈ° μ μ©μΌλ‘ μ¬μ© κΆμ₯
νν μ€μ
// β μλͺ»λ¨: μμ±μμμ νΈμΆ
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
private currentUser?: ContextUser;
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
// μμ±μμμλ Context μμ
const context = Sonamu.getContext(); // β μλ¬
this.currentUser = context.user;
}
}
// β μλͺ»λ¨: ν΄λμ€ νλμμ νΈμΆ
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
// ν΄λμ€ νλ μ΄κΈ°ν μμ μλ Context μμ
private context = Sonamu.getContext(); // β μλ¬
}
// β μλͺ»λ¨: API λ©μλ λ°μμ νΈμΆ
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
private getContext() {
// private λ©μλλ κ°λ₯νμ§λ§,
// API μμ² μ»¨ν
μ€νΈ λ°μμ νΈμΆνλ©΄ μλ¬
return Sonamu.getContext();
}
}
// β
μ¬λ°λ¦: API λ©μλ λ΄μμ νΈμΆ
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(); // β μ μ
// ...
}
private async helperMethod(): Promise<void> {
// API λ©μλμμ νΈμΆλ κ²½μ° μ μ
const context = Sonamu.getContext(); // β μ μ
// ...
}
}
λ€μ λ¨κ³
SonamuContext
Context ꡬ쑰 μ΄ν΄νκΈ°
컀μ€ν Context
Context νμ₯νκΈ°
@api λ°μ½λ μ΄ν°
API κΈ°λ³Έ μ¬μ©λ²
μλ¬ μ²λ¦¬
API μλ¬ νΈλ€λ§