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 ์ฌ์ฉ ์ ์ฃผ์์ฌํญ:
- API ๋ฉ์๋ ๋ด์์๋ง ํธ์ถ ๊ฐ๋ฅ
- ์์ฑ์๋ ํด๋์ค ํ๋์์ ํธ์ถ ๋ถ๊ฐ
- ๋น๋๊ธฐ ๊ฒฝ๊ณ๋ฅผ ๋์ด๋ ๊ฐ์ Context ์ ์ง
- ์ฌ๋ฌ ๋ฒ ํธ์ถํด๋ ๊ฐ์ ๊ฐ์ฒด ๋ฐํ
- 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(); // โ ์ ์
// ...
}
}