๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
API ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ 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: "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 ์—๋Ÿฌ ํ•ธ๋“ค๋ง