sonamu 0.10.1 → 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.
- package/dist/api/sonamu.js +1 -1
- package/dist/bin/build-config.d.ts.map +1 -1
- package/dist/bin/build-config.js +3 -3
- package/dist/database/puri.types.d.ts +4 -1
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +1 -1
- package/dist/ui-web/assets/index-CDd6xT-F.js +170 -0
- package/dist/ui-web/index.html +1 -1
- package/package.json +3 -3
- package/src/api/__tests__/sonamu.websocket.test.ts +100 -2
- package/src/api/sonamu.ts +3 -3
- package/src/bin/build-config.ts +3 -2
- package/src/database/puri.types.ts +7 -2
- package/src/template/generated.template.test-d.ts +21 -1
- package/dist/ui-web/assets/index-DFStGyd0.js +0 -202
package/dist/ui-web/index.html
CHANGED
|
@@ -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-
|
|
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.
|
|
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/
|
|
135
|
-
"@sonamu-kit/
|
|
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 {
|
|
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)
|
|
1099
|
-
// 2)
|
|
1100
|
-
// 3)
|
|
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 (
|
package/src/bin/build-config.ts
CHANGED
|
@@ -32,7 +32,8 @@ export const WEB_ARTIFACTS: BuildArtifact[] = [
|
|
|
32
32
|
description: "Web 프로젝트 클라이언트 빌드 산출물",
|
|
33
33
|
projectPath: "web",
|
|
34
34
|
preBuildCommand: () => "rm -rf dist/client",
|
|
35
|
-
buildCommand: () =>
|
|
35
|
+
buildCommand: () =>
|
|
36
|
+
"tsc -b --noEmit && vite build --config vite.config.ts --outDir dist/client",
|
|
36
37
|
},
|
|
37
38
|
{
|
|
38
39
|
name: "Web Server",
|
|
@@ -40,7 +41,7 @@ export const WEB_ARTIFACTS: BuildArtifact[] = [
|
|
|
40
41
|
projectPath: "web",
|
|
41
42
|
preBuildCommand: () => "rm -rf dist/server",
|
|
42
43
|
buildCommand: () =>
|
|
43
|
-
"tsc --noEmit && vite build --config vite.config.ts --ssr src/entry-server.generated.tsx --outDir dist/server",
|
|
44
|
+
"tsc -b --noEmit && vite build --config vite.config.ts --ssr src/entry-server.generated.tsx --outDir dist/server",
|
|
44
45
|
postBuildCommand: () =>
|
|
45
46
|
"rm -rf ../api/web-dist && mkdir -p ../api/web-dist && cp -r dist/* ../api/web-dist",
|
|
46
47
|
},
|
|
@@ -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
|
-
//
|
|
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
|
+
});
|