@simplysm/service-server 13.0.96 → 13.0.98

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.
@@ -1,249 +0,0 @@
1
- # 내장 서비스 상세 API
2
-
3
- ## OrmService
4
-
5
- 서비스 이름: `"Orm"`. 인증 필수 (`auth` 래핑). WebSocket 전용 -- HTTP로는 사용 불가.
6
-
7
- 소켓별로 DB 커넥션을 `WeakMap<ServiceSocket, Map<number, DbConn>>`으로 관리한다. 소켓 `close` 이벤트 시 해당 소켓의 모든 커넥션을 자동 정리한다.
8
-
9
- ### 메서드
10
-
11
- ```typescript
12
- // DB 정보 조회 (연결 없이)
13
- async getInfo(opt: DbConnOptions & { configName: string }): Promise<{
14
- dialect: Dialect; // "mysql" | "postgresql" | "mssql" (mssql-azure는 mssql로 반환)
15
- database?: string;
16
- schema?: string;
17
- }>
18
-
19
- // DB 연결 생성 (connId 반환)
20
- async connect(opt: DbConnOptions & { configName: string }): Promise<number>
21
-
22
- // DB 연결 종료
23
- async close(connId: number): Promise<void>
24
-
25
- // 트랜잭션 제어
26
- async beginTransaction(connId: number, isolationLevel?: IsolationLevel): Promise<void>
27
- async commitTransaction(connId: number): Promise<void>
28
- async rollbackTransaction(connId: number): Promise<void>
29
-
30
- // 파라미터화된 쿼리 실행
31
- async executeParametrized(
32
- connId: number,
33
- query: string,
34
- params?: unknown[],
35
- ): Promise<unknown[][]>
36
-
37
- // QueryDef 기반 쿼리 실행 (쿼리 빌더 사용)
38
- async executeDefs(
39
- connId: number,
40
- defs: QueryDef[],
41
- options?: (ResultMeta | undefined)[],
42
- ): Promise<unknown[][]>
43
-
44
- // 벌크 인서트
45
- async bulkInsert(
46
- connId: number,
47
- tableName: string,
48
- columnDefs: Record<string, ColumnMeta>,
49
- records: Record<string, unknown>[],
50
- ): Promise<void>
51
- ```
52
-
53
- ### DbConnOptions
54
-
55
- `@simplysm/service-common`에서 가져온다.
56
-
57
- ```typescript
58
- type DbConnOptions = {
59
- configName?: string;
60
- config?: Record<string, unknown>; // .config.json 설정 오버라이드
61
- };
62
- ```
63
-
64
- ### 설정 예시
65
-
66
- `.config.json`:
67
-
68
- ```json
69
- {
70
- "orm": {
71
- "main": {
72
- "dialect": "mysql",
73
- "host": "localhost",
74
- "port": 3306,
75
- "username": "root",
76
- "password": "password",
77
- "database": "mydb"
78
- },
79
- "secondary": {
80
- "dialect": "postgresql",
81
- "host": "pg-host",
82
- "port": 5432,
83
- "username": "admin",
84
- "password": "password",
85
- "database": "mydb",
86
- "schema": "public"
87
- }
88
- }
89
- }
90
- ```
91
-
92
- ### 타입 내보내기
93
-
94
- ```typescript
95
- import type { OrmServiceType } from "@simplysm/service-server";
96
- // 클라이언트에서 타입 공유: client.getService<OrmServiceType>("Orm")
97
- ```
98
-
99
- ---
100
-
101
- ## AutoUpdateService
102
-
103
- 서비스 이름: `"AutoUpdate"`. 인증 불필요. `clientPath/{platform}/updates/` 디렉토리에서 semver 기준 최신 버전 파일을 탐색한다.
104
-
105
- ### 메서드
106
-
107
- ```typescript
108
- async getLastVersion(platform: string): Promise<{
109
- version: string; // 최신 버전 문자열 (예: "1.2.3")
110
- downloadPath: string; // 다운로드 경로 (예: "/my-app/android/updates/1.2.3.apk")
111
- } | undefined>
112
- ```
113
-
114
- ### 지원 플랫폼
115
-
116
- | platform | 파일 확장자 | 디렉토리 경로 |
117
- |----------|------------|---------------|
118
- | `"android"` | `.apk` | `www/{clientName}/android/updates/` |
119
- | 기타 | `.exe` | `www/{clientName}/{platform}/updates/` |
120
-
121
- 파일명은 버전 번호여야 한다 (예: `1.2.3.apk`, `2.0.0.exe`). `semver.maxSatisfying`으로 최신 버전을 결정한다.
122
-
123
- ### V1 레거시 호환
124
-
125
- V1 프로토콜 클라이언트(WebSocket `ver` 쿼리 파라미터 없음)도 `SdAutoUpdateService.getLastVersion` 명령으로 자동 업데이트를 요청할 수 있다. V1 클라이언트의 다른 요청은 `UPGRADE_REQUIRED` 에러를 반환한다.
126
-
127
- ### 타입 내보내기
128
-
129
- ```typescript
130
- import type { AutoUpdateServiceType } from "@simplysm/service-server";
131
- ```
132
-
133
- ---
134
-
135
- ## SmtpClientService
136
-
137
- 서비스 이름: `"SmtpClient"`. 인증 불필요. nodemailer 기반 이메일 전송.
138
-
139
- ### 메서드
140
-
141
- ```typescript
142
- // 직접 SMTP 설정으로 전송
143
- async send(options: SmtpClientSendOption): Promise<string>
144
- // 반환값: messageId
145
-
146
- // .config.json의 smtp 설정으로 전송
147
- async sendByConfig(configName: string, options: SmtpClientSendByDefaultOption): Promise<string>
148
- // 반환값: messageId
149
- ```
150
-
151
- ### SmtpClientSendOption
152
-
153
- `@simplysm/service-common`에서 가져온다.
154
-
155
- ```typescript
156
- interface SmtpClientSendOption {
157
- host: string;
158
- port?: number;
159
- secure?: boolean;
160
- user?: string;
161
- pass?: string;
162
- from: string;
163
- to: string;
164
- cc?: string;
165
- bcc?: string;
166
- subject: string;
167
- html: string;
168
- attachments?: SmtpClientSendAttachment[];
169
- }
170
- ```
171
-
172
- ### SmtpClientSendByDefaultOption
173
-
174
- ```typescript
175
- interface SmtpClientSendByDefaultOption {
176
- to: string;
177
- cc?: string;
178
- bcc?: string;
179
- subject: string;
180
- html: string;
181
- attachments?: SmtpClientSendAttachment[];
182
- }
183
- ```
184
-
185
- ### SmtpClientSendAttachment
186
-
187
- ```typescript
188
- interface SmtpClientSendAttachment {
189
- filename: string;
190
- content?: string | Uint8Array;
191
- path?: any;
192
- contentType?: string;
193
- }
194
- ```
195
-
196
- ### 설정 예시
197
-
198
- `.config.json`:
199
-
200
- ```json
201
- {
202
- "smtp": {
203
- "default": {
204
- "host": "smtp.example.com",
205
- "port": 587,
206
- "secure": false,
207
- "user": "noreply@example.com",
208
- "pass": "password",
209
- "senderName": "My App",
210
- "senderEmail": "noreply@example.com"
211
- }
212
- }
213
- }
214
- ```
215
-
216
- `senderEmail`이 없으면 `user`가 발신자 이메일로 사용된다.
217
-
218
- ### 사용 예시
219
-
220
- ```typescript
221
- // 서버에서 직접 사용
222
- const ctx = createServiceContext(server);
223
- const methods = SmtpClientService.factory(ctx);
224
-
225
- // 직접 설정
226
- await methods.send({
227
- host: "smtp.example.com",
228
- port: 587,
229
- user: "user@example.com",
230
- pass: "pass",
231
- from: '"My App" <noreply@example.com>',
232
- to: "recipient@example.com",
233
- subject: "테스트",
234
- html: "<p>Hello</p>",
235
- });
236
-
237
- // .config.json 기반
238
- await methods.sendByConfig("default", {
239
- to: "recipient@example.com",
240
- subject: "테스트",
241
- html: "<p>Hello</p>",
242
- });
243
- ```
244
-
245
- ### 타입 내보내기
246
-
247
- ```typescript
248
- import type { SmtpClientServiceType } from "@simplysm/service-server";
249
- ```
@@ -1,200 +0,0 @@
1
- # 전송 계층 및 프로토콜
2
-
3
- ## WebSocket 전송
4
-
5
- ### WebSocketHandler
6
-
7
- 다수의 WebSocket 연결을 관리하고, 메시지를 서비스로 라우팅하며, 이벤트 브로드캐스트를 처리한다.
8
-
9
- ```typescript
10
- interface WebSocketHandler {
11
- addSocket(socket: WebSocket, clientId: string, clientName: string, connReq: FastifyRequest): void;
12
- closeAll(): void;
13
- broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>;
14
- emit<TInfo, TData>(
15
- eventDef: ServiceEventDef<TInfo, TData>,
16
- infoSelector: (item: TInfo) => boolean,
17
- data: TData,
18
- ): Promise<void>;
19
- }
20
-
21
- function createWebSocketHandler(
22
- runMethod: (def: {
23
- serviceName: string;
24
- methodName: string;
25
- params: unknown[];
26
- socket?: ServiceSocket;
27
- }) => Promise<unknown>,
28
- jwtSecret: string | undefined,
29
- ): WebSocketHandler;
30
- ```
31
-
32
- ### WebSocket 연결 흐름
33
-
34
- 1. 클라이언트가 `/` 또는 `/ws`로 WebSocket 연결
35
- 2. 쿼리 파라미터로 프로토콜 버전 구분:
36
- - `ver=2&clientId=...&clientName=...` -- V2 프로토콜 (현재)
37
- - 쿼리 없음 -- V1 레거시 (AutoUpdate만 지원)
38
- 3. V2: `createServiceSocket`으로 소켓 래핑, 메시지 라우팅 시작
39
- 4. 같은 `clientId`로 재연결 시 이전 소켓을 종료하고 새 소켓으로 교체
40
-
41
- ### WebSocket 메시지 프로토콜
42
-
43
- V2 클라이언트 메시지 포맷 (`ServiceClientMessage`):
44
-
45
- | `name` | `body` | 설명 |
46
- |--------|--------|------|
47
- | `"{Service}.{Method}"` | `unknown[]` (파라미터 배열) | 서비스 메서드 호출 |
48
- | `"auth"` | `string` (JWT 토큰) | 인증 토큰 설정 |
49
- | `"evt:add"` | `{ key, name, info }` | 이벤트 리스너 등록 |
50
- | `"evt:remove"` | `{ key }` | 이벤트 리스너 제거 |
51
- | `"evt:gets"` | `{ name }` | 이벤트 리스너 목록 조회 |
52
- | `"evt:emit"` | `{ keys, data }` | 대상 키에 이벤트 전송 |
53
-
54
- V2 서버 응답 (`ServiceServerMessage`):
55
-
56
- | `name` | `body` | 설명 |
57
- |--------|--------|------|
58
- | `"response"` | `unknown` (결과) | 성공 응답 |
59
- | `"error"` | `{ name, message, code, stack }` | 에러 응답 |
60
- | `"progress"` | `{ totalSize, completedSize }` | 전송 진행률 |
61
- | `"evt:on"` | `{ keys, data }` | 이벤트 수신 |
62
- | `"reload"` | `{ clientName, changedFileSet }` | 리로드 알림 |
63
-
64
- ---
65
-
66
- ### ServiceSocket
67
-
68
- 단일 WebSocket 연결을 관리하는 인터페이스. 프로토콜 인코딩/디코딩, ping/pong keep-alive(5초 간격), 이벤트 리스너 추적을 담당한다.
69
-
70
- ```typescript
71
- interface ServiceSocket {
72
- readonly connectedAtDateTime: DateTime;
73
- readonly clientName: string;
74
- readonly connReq: FastifyRequest;
75
- authTokenPayload?: AuthTokenPayload;
76
-
77
- close(): void;
78
- send(uuid: string, msg: ServiceServerMessage): Promise<number>;
79
-
80
- // 이벤트 리스너 관리
81
- addListener(key: string, eventName: string, info: unknown): void;
82
- removeListener(key: string): void;
83
- getEventListeners(eventName: string): Array<{ key: string; info: unknown }>;
84
- filterEventTargetKeys(targetKeys: string[]): string[];
85
-
86
- // 이벤트 핸들러 등록
87
- on(event: "error", handler: (err: Error) => void): void;
88
- on(event: "close", handler: (code: number) => void): void;
89
- on(event: "message", handler: (data: { uuid: string; msg: ServiceClientMessage }) => void): void;
90
- }
91
-
92
- function createServiceSocket(
93
- socket: WebSocket,
94
- clientId: string,
95
- clientName: string,
96
- connReq: FastifyRequest,
97
- ): ServiceSocket;
98
- ```
99
-
100
- ---
101
-
102
- ## HTTP 전송
103
-
104
- ### handleHttpRequest
105
-
106
- HTTP API 요청 처리 함수. 서버 내부에서 `/api/:service/:method` 라우트에 등록된다.
107
-
108
- ```typescript
109
- async function handleHttpRequest<TAuthInfo>(
110
- req: FastifyRequest,
111
- reply: FastifyReply,
112
- jwtSecret: string | undefined,
113
- runMethod: (def: {
114
- serviceName: string;
115
- methodName: string;
116
- params: unknown[];
117
- http: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> };
118
- }) => Promise<unknown>,
119
- ): Promise<void>;
120
- ```
121
-
122
- - GET: `?json=` 쿼리 파라미터에서 파라미터 배열을 JSON 파싱
123
- - POST: request body를 배열로 파싱
124
- - `x-sd-client-name` 헤더 필수
125
- - `Authorization: Bearer <token>` 헤더가 있으면 JWT 검증 후 `authTokenPayload`로 전달
126
-
127
- ### handleUpload
128
-
129
- 파일 업로드 처리 함수. `/upload` 라우트에 등록된다. 인증 필수.
130
-
131
- ```typescript
132
- async function handleUpload(
133
- req: FastifyRequest,
134
- reply: FastifyReply,
135
- rootPath: string,
136
- jwtSecret: string | undefined,
137
- ): Promise<void>;
138
- ```
139
-
140
- - multipart/form-data 필수
141
- - 파일을 `rootPath/www/uploads/{UUID}.{ext}`로 저장
142
- - 업로드 실패 시 불완전한 파일 자동 삭제
143
- - 응답: `ServiceUploadResult[]`
144
-
145
- ### handleStaticFile
146
-
147
- 정적 파일 서빙 함수. 와일드카드 라우트 `/*`에 등록된다.
148
-
149
- ```typescript
150
- async function handleStaticFile(
151
- req: FastifyRequest,
152
- reply: FastifyReply,
153
- rootPath: string,
154
- urlPath: string,
155
- ): Promise<void>;
156
- ```
157
-
158
- - `rootPath/www/` 디렉토리 기준
159
- - 디렉토리 접근 시 trailing slash 리다이렉트 + `index.html` 서빙
160
- - `.`으로 시작하는 파일은 403 거부
161
- - path traversal 방지 (보안 가드)
162
-
163
- ---
164
-
165
- ## 프로토콜 래퍼
166
-
167
- 메시지 인코딩/디코딩을 자동으로 워커 스레드에 오프로드한다. 30KB 이상의 메시지 또는 `Uint8Array` 포함 메시지는 워커에서 처리하여 메인 스레드 블로킹을 방지한다.
168
-
169
- ```typescript
170
- interface ServerProtocolWrapper {
171
- encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
172
- decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
173
- dispose(): void;
174
- }
175
-
176
- function createServerProtocolWrapper(): ServerProtocolWrapper;
177
- ```
178
-
179
- ### 워커 오프로드 조건
180
-
181
- - **인코딩**: 메시지 body가 `Uint8Array`이거나, 배열 내에 `Uint8Array` 요소가 있으면 워커 사용
182
- - **디코딩**: 메시지 크기가 30KB 초과이면 워커 사용
183
- - 워커는 lazy singleton으로 관리 (최대 4GB 메모리 제한)
184
-
185
- ---
186
-
187
- ## 설정 관리
188
-
189
- ### getConfig
190
-
191
- `.config.json` 파일을 읽고 캐시하는 함수.
192
-
193
- ```typescript
194
- async function getConfig<TConfig>(filePath: string): Promise<TConfig | undefined>;
195
- ```
196
-
197
- - LRU 캐시: 1시간 만료, 10분마다 GC
198
- - 파일 감시(FsWatcher)로 변경 시 자동 리로드
199
- - 파일 삭제 시 캐시에서 제거 및 감시 해제
200
- - 캐시 만료 시 감시도 함께 해제