sonamu 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/sonamu-ui/setting.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>{{projectName}}: Sonamu UI</title>
8
- <script type="module" crossorigin src="/sonamu-ui/assets/index-DFStGyd0.js"></script>
8
+ <script type="module" crossorigin src="/sonamu-ui/assets/index-CDd6xT-F.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-Dx4ap5i4.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "framework",
@@ -131,8 +131,8 @@
131
131
  "vitest": "^4.1.9",
132
132
  "@sonamu-kit/hmr-hook": "^0.5.1",
133
133
  "@sonamu-kit/hmr-runner": "^0.2.0",
134
- "@sonamu-kit/tasks": "^0.3.1",
135
- "@sonamu-kit/ts-loader": "^2.2.0"
134
+ "@sonamu-kit/ts-loader": "^2.2.0",
135
+ "@sonamu-kit/tasks": "^0.3.1"
136
136
  },
137
137
  "devDependencies": {
138
138
  "@types/bcrypt": "^6",
@@ -1,11 +1,109 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { type FastifyRequest } from "fastify";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { type WebSocket } from "ws";
4
+ import { z } from "zod";
2
5
 
3
- import { type AnyWebSocketConnection } from "../../stream/ws";
6
+ import { type AnyWebSocketConnection, WebSocketRuntime } from "../../stream/ws";
4
7
  import { type WebSocketTelemetryConnectionContext } from "../../stream/ws-telemetry";
8
+ import { type SonamuFastifyConfig } from "../config";
5
9
  import { type WebSocketContext } from "../context";
10
+ import { type ExtendedApi } from "../decorators";
6
11
  import { Sonamu } from "../sonamu";
7
12
 
8
13
  describe("Sonamu websocket context scoping", () => {
14
+ it("웹소켓 컨텍스트를 생성한 뒤 해당 컨텍스트 안에서 guard를 실행한다", async () => {
15
+ const executionOrder: string[] = [];
16
+ const rawWs = {
17
+ id: "ws-guard",
18
+ namespace: "chat",
19
+ transport: "ws" as const,
20
+ closed: false,
21
+ publishUntyped() {},
22
+ close() {},
23
+ onClose() {},
24
+ onMessage() {},
25
+ publish() {},
26
+ waitForClose() {
27
+ return Promise.resolve();
28
+ },
29
+ join() {},
30
+ leave() {},
31
+ setUserId() {},
32
+ clearUserId() {},
33
+ } satisfies AnyWebSocketConnection;
34
+ const runtime = new WebSocketRuntime({ nodeId: "websocket-guard-test" });
35
+ const originalRuntime = Reflect.get(Sonamu, "_websocketRuntime");
36
+ const originalSyncer = Reflect.get(Sonamu, "_syncer");
37
+ let contextCreated: WebSocketContext | null = null;
38
+ let guardContext: WebSocketContext | null = null;
39
+
40
+ vi.spyOn(runtime, "registerConnection").mockReturnValue(rawWs);
41
+ vi.spyOn(runtime, "activateConnection").mockImplementation(() => {
42
+ executionOrder.push("activate");
43
+ });
44
+ vi.spyOn(Sonamu, "createWebSocketContext").mockImplementation(async (_config, request, ws) => {
45
+ executionOrder.push("context");
46
+ contextCreated = {
47
+ transport: "ws",
48
+ request,
49
+ headers: request.headers,
50
+ ws,
51
+ naiteStore: new Map(),
52
+ locale: "ko",
53
+ user: null,
54
+ session: null,
55
+ };
56
+ return contextCreated;
57
+ });
58
+ vi.spyOn(Sonamu, "invokeModelMethod").mockImplementation(async () => {
59
+ executionOrder.push("handler");
60
+ });
61
+
62
+ const config = {
63
+ guardHandler() {
64
+ executionOrder.push("guard");
65
+ guardContext = Sonamu.getContext<WebSocketContext>();
66
+ },
67
+ } as SonamuFastifyConfig;
68
+ const api: ExtendedApi = {
69
+ modelName: "ChatFrame",
70
+ methodName: "subscribe",
71
+ path: "/chat/subscribe",
72
+ options: { guards: ["user"] },
73
+ websocketOptions: {
74
+ outEvents: z.object({}),
75
+ inEvents: z.object({}),
76
+ },
77
+ typeParameters: [],
78
+ parameters: [],
79
+ returnType: "void",
80
+ };
81
+ const request = {
82
+ headers: {},
83
+ query: {},
84
+ } as FastifyRequest;
85
+ const createWebSocketHandler: (
86
+ api: ExtendedApi,
87
+ config: SonamuFastifyConfig,
88
+ ) => (connection: { socket: WebSocket }, request: FastifyRequest) => Promise<void> =
89
+ Reflect.get(Sonamu, "createWebSocketHandler");
90
+
91
+ try {
92
+ Sonamu.websocketRuntime = runtime;
93
+ Reflect.set(Sonamu, "_syncer", { types: {} });
94
+
95
+ await createWebSocketHandler.call(Sonamu, api, config)({ socket: {} as WebSocket }, request);
96
+
97
+ expect(executionOrder).toEqual(["context", "guard", "activate", "handler"]);
98
+ expect(guardContext).toBe(contextCreated);
99
+ expect(guardContext?.transport).toBe("ws");
100
+ } finally {
101
+ Reflect.set(Sonamu, "_websocketRuntime", originalRuntime);
102
+ Reflect.set(Sonamu, "_syncer", originalSyncer);
103
+ vi.restoreAllMocks();
104
+ }
105
+ });
106
+
9
107
  it("restores websocket context inside deferred message handlers", async () => {
10
108
  const messageHandlers = new Map<
11
109
  string,
package/src/api/sonamu.ts CHANGED
@@ -1095,9 +1095,9 @@ class SonamuClass {
1095
1095
  }
1096
1096
 
1097
1097
  // WS route 핸들러의 실행 순서를 고정함:
1098
- // 1) guard를 connection 등록 이전에 돌려 인증 실패 부분 등록 상태를 남기지 않음
1099
- // 2) query param 파싱도 activation 전에 끝내 handshake 실패가 registry에 노출되지 않게
1100
- // 3) `active: false`로 먼저 등록하고, context 준비가 끝난 뒤 `activate()`해 브로드캐스트가 초기화 중간 상태를 보지 못하게 함
1098
+ // 1) query param 파싱을 activation 전에 끝내 handshake 실패가 registry에 노출되지 않게
1099
+ // 2) `active: false`로 먼저 등록한 context 안에서 guard를 실행해 인증 정보를 참조할 수 있게
1100
+ // 3) context/guard 준비가 끝난 뒤 `activate()`해 브로드캐스트가 초기화 중간 상태를 보지 못하게 함
1101
1101
  // 에러 발생 시에는 resolveWebSocketCloseDescriptor 정책에 따라 close code를 매핑함
1102
1102
  private createWebSocketHandler(api: ExtendedApi, config: SonamuFastifyConfig) {
1103
1103
  return async (
@@ -340,10 +340,15 @@ type GeneratedKeys<T> = T extends { __generated__: readonly (infer K)[] }
340
340
  ? Extract<K, keyof PuriTable<T>>
341
341
  : never;
342
342
 
343
- // Insert 타입: 메타데이터 제거 후, __hasDefault__ 컬럼들만 optional로 처리, __generated__ 컬럼은 완전히 제외
343
+ // __virtual_query__에 포함된 키들 (INSERT 제외해야 함)
344
+ type VirtualQueryKeys<T> = T extends { __virtual_query__: readonly (infer K)[] }
345
+ ? Extract<K, keyof PuriTable<T>>
346
+ : never;
347
+
348
+ // Insert 타입: 메타데이터 제거 후, __hasDefault__ 컬럼들만 optional로 처리, generated/virtualQuery 컬럼은 완전히 제외
344
349
  export type InsertData<T> = Omit<
345
350
  PuriTable<T>,
346
- InternalTypeKeys | HasDefaultKeys<T> | GeneratedKeys<T>
351
+ InternalTypeKeys | HasDefaultKeys<T> | GeneratedKeys<T> | VirtualQueryKeys<T>
347
352
  > & {
348
353
  [K in HasDefaultKeys<T>]?: PuriTable<T>[K];
349
354
  };
@@ -13,11 +13,20 @@ type GeneratedTemplateRow = {
13
13
  __generated__: readonly ["slug", "search_text"];
14
14
  };
15
15
 
16
+ type VirtualQueryTemplateRow = {
17
+ id: number;
18
+ title: string;
19
+ required_virtual_value: string;
20
+ nullable_virtual_value: string | null;
21
+ __hasDefault__: readonly ["id"];
22
+ __virtual_query__: readonly ["required_virtual_value", "nullable_virtual_value"];
23
+ };
24
+
16
25
  describe("generated template __generated__ metadata", () => {
17
26
  it("searchText와 기존 generated 컬럼을 write payload에서 제외해야 한다", () => {
18
27
  type Result = InsertData<GeneratedTemplateRow>;
19
28
 
20
- expectTypeOf<Result>().toEqualTypeOf<{
29
+ expectTypeOf<Result>().branded.toEqualTypeOf<{
21
30
  id?: number;
22
31
  title: string;
23
32
  code: string;
@@ -45,3 +54,14 @@ describe("generated template __generated__ metadata", () => {
45
54
  }>();
46
55
  });
47
56
  });
57
+
58
+ describe("generated template __virtual_query__ metadata", () => {
59
+ it("virtualQuery 컬럼을 nullability와 무관하게 write payload에서 제외해야 한다", () => {
60
+ type Result = InsertData<VirtualQueryTemplateRow>;
61
+
62
+ expectTypeOf<Result>().branded.toEqualTypeOf<{
63
+ id?: number;
64
+ title: string;
65
+ }>();
66
+ });
67
+ });