@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.
- package/README.md +79 -345
- package/docs/auth.md +48 -0
- package/docs/core.md +161 -0
- package/docs/server.md +206 -0
- package/docs/services.md +176 -0
- package/docs/transport.md +152 -0
- package/package.json +8 -8
- package/docs/builtin-services.md +0 -249
- package/docs/transport-protocol.md +0 -200
package/docs/builtin-services.md
DELETED
|
@@ -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
|
-
- 캐시 만료 시 감시도 함께 해제
|