create-entity-server 0.0.15 → 0.0.23
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/bin/create.js +15 -7
- package/package.json +1 -1
- package/template/.env.example +8 -7
- package/template/configs/database.json +173 -10
- package/template/entities/Account/account_audit.json +4 -5
- package/template/entities/System/system_audit_log.json +14 -8
- package/template/samples/README.md +28 -22
- package/template/samples/browser/entity-server-client.js +453 -0
- package/template/samples/browser/example.html +498 -0
- package/template/samples/entities/02_types_and_defaults.json +15 -16
- package/template/samples/entities/04_fk_and_composite_unique.json +0 -2
- package/template/samples/entities/05_cache.json +9 -8
- package/template/samples/entities/06_history_and_hard_delete.json +27 -9
- package/template/samples/entities/07_license_scope.json +40 -31
- package/template/samples/entities/09_hook_entity.json +0 -6
- package/template/samples/entities/10_hook_submit_delete.json +5 -2
- package/template/samples/entities/11_hook_webhook.json +9 -7
- package/template/samples/entities/12_hook_push.json +3 -3
- package/template/samples/entities/13_read_only.json +13 -10
- package/template/samples/entities/15_reset_defaults.json +0 -1
- package/template/samples/entities/16_isolated_license.json +62 -0
- package/template/samples/entities/README.md +36 -39
- package/template/samples/flutter/lib/entity_server_client.dart +170 -48
- package/template/samples/java/EntityServerClient.java +208 -61
- package/template/samples/java/EntityServerExample.java +4 -3
- package/template/samples/kotlin/EntityServerClient.kt +175 -45
- package/template/samples/node/src/EntityServerClient.js +232 -59
- package/template/samples/node/src/example.js +9 -9
- package/template/samples/php/ci4/Config/EntityServer.php +0 -1
- package/template/samples/php/ci4/Libraries/EntityServer.php +206 -53
- package/template/samples/php/laravel/Services/EntityServerService.php +190 -41
- package/template/samples/python/entity_server.py +181 -68
- package/template/samples/python/example.py +7 -6
- package/template/samples/react/src/example.tsx +41 -25
- package/template/samples/swift/EntityServerClient.swift +143 -37
- package/template/scripts/run.ps1 +12 -3
- package/template/scripts/run.sh +12 -8
- package/template/scripts/update-server.ps1 +68 -2
- package/template/scripts/update-server.sh +59 -2
- package/template/samples/entities/order_notification.json +0 -51
- package/template/samples/react/src/api/entityServerClient.ts +0 -413
- package/template/samples/react/src/hooks/useEntity.ts +0 -173
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 환경변수:
|
|
8
8
|
* ENTITY_SERVER_URL http://localhost:47200
|
|
9
|
-
* ENTITY_SERVER_API_KEY your-api-key
|
|
10
|
-
* ENTITY_SERVER_HMAC_SECRET your-hmac-secret
|
|
11
|
-
*
|
|
9
|
+
* ENTITY_SERVER_API_KEY your-api-key (HMAC 모드)
|
|
10
|
+
* ENTITY_SERVER_HMAC_SECRET your-hmac-secret (HMAC 모드)
|
|
11
|
+
* ENTITY_SERVER_TOKEN your-jwt-token (JWT 모드)
|
|
12
12
|
*
|
|
13
13
|
* 사용 예:
|
|
14
14
|
* const es = new EntityServerClient();
|
|
@@ -27,27 +27,59 @@
|
|
|
27
27
|
* }
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { createHmac, randomUUID } from "crypto";
|
|
30
|
+
import { createHmac, randomFillSync, randomUUID } from "crypto";
|
|
31
31
|
import { xchacha20_poly1305 } from "@noble/ciphers/chacha";
|
|
32
32
|
import { sha256 } from "@noble/hashes/sha2";
|
|
33
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
33
34
|
|
|
34
35
|
export class EntityServerClient {
|
|
35
36
|
#baseUrl;
|
|
36
37
|
#apiKey;
|
|
37
38
|
#hmacSecret;
|
|
38
|
-
#
|
|
39
|
+
#token;
|
|
40
|
+
#encryptRequests;
|
|
41
|
+
#packetEncryption = false;
|
|
39
42
|
#activeTxId = null;
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @param {Object} [opts]
|
|
46
|
+
* @param {string} [opts.baseUrl] ENTITY_SERVER_URL 환경변수 또는 기본값
|
|
47
|
+
* @param {string} [opts.apiKey] X-API-Key 헤더값 (HMAC 모드)
|
|
48
|
+
* @param {string} [opts.hmacSecret] HMAC 서명 시크릿 (HMAC 모드)
|
|
49
|
+
* @param {string} [opts.token] JWT Bearer 토큰 (JWT 모드)
|
|
50
|
+
* @param {boolean} [opts.encryptRequests] true 이면 POST 요청 바디를 XChaCha20-Poly1305로 암호화
|
|
51
|
+
*/
|
|
41
52
|
constructor({
|
|
42
53
|
baseUrl = process.env.ENTITY_SERVER_URL ?? "http://localhost:47200",
|
|
43
54
|
apiKey = process.env.ENTITY_SERVER_API_KEY ?? "",
|
|
44
55
|
hmacSecret = process.env.ENTITY_SERVER_HMAC_SECRET ?? "",
|
|
45
|
-
|
|
56
|
+
token = process.env.ENTITY_SERVER_TOKEN ?? "",
|
|
57
|
+
encryptRequests = false,
|
|
46
58
|
} = {}) {
|
|
47
59
|
this.#baseUrl = baseUrl.replace(/\/$/, "");
|
|
48
60
|
this.#apiKey = apiKey;
|
|
49
61
|
this.#hmacSecret = hmacSecret;
|
|
50
|
-
this.#
|
|
62
|
+
this.#token = token;
|
|
63
|
+
this.#encryptRequests = encryptRequests;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** JWT Bearer 토큰을 설정합니다. HMAC 모드와 배타적으로 사용합니다. */
|
|
67
|
+
setToken(token) {
|
|
68
|
+
this.#token = token;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 서버 헬스 체크를 수행하고 패킷 암호화 활성 여부를 자동으로 감지합니다.
|
|
73
|
+
* 서버가 packet_encryption: true 를 응답하면 이후 모든 요청에 암호화가 자동 적용됩니다.
|
|
74
|
+
* @returns {Promise<{ok: boolean, packet_encryption?: boolean}>}
|
|
75
|
+
*/
|
|
76
|
+
async checkHealth() {
|
|
77
|
+
const res = await fetch(this.#baseUrl + "/v1/health");
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
if (data.packet_encryption) {
|
|
80
|
+
this.#packetEncryption = true;
|
|
81
|
+
}
|
|
82
|
+
return data;
|
|
51
83
|
}
|
|
52
84
|
|
|
53
85
|
// ─── 트랜잭션 ─────────────────────────────────────────────────────────────
|
|
@@ -89,38 +121,91 @@ export class EntityServerClient {
|
|
|
89
121
|
|
|
90
122
|
// ─── CRUD ────────────────────────────────────────────────────────────────
|
|
91
123
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
124
|
+
/**
|
|
125
|
+
* 단건 조회
|
|
126
|
+
* @param {Object} [opts]
|
|
127
|
+
* @param {boolean} [opts.skipHooks] true 이면 after_get 훅 미실행
|
|
128
|
+
*/
|
|
129
|
+
get(entity, seq, { skipHooks = false } = {}) {
|
|
130
|
+
const q = skipHooks ? "?skipHooks=true" : "";
|
|
131
|
+
return this.#request("GET", `/v1/entity/${entity}/${seq}${q}`);
|
|
95
132
|
}
|
|
96
133
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
/**
|
|
135
|
+
* 조건으로 단건 조회 (POST + conditions body)
|
|
136
|
+
*
|
|
137
|
+
* @param {string} entity 엔티티 이름
|
|
138
|
+
* @param {Object} conditions 필터 조건. index/hash/unique 필드만 사용 가능
|
|
139
|
+
* @param {Object} [opts]
|
|
140
|
+
* @param {boolean} [opts.skipHooks] after_find 훅 미실행 여부 (기본 false)
|
|
141
|
+
*/
|
|
142
|
+
find(entity, conditions, { skipHooks = false } = {}) {
|
|
143
|
+
const q = skipHooks ? "?skipHooks=true" : "";
|
|
144
|
+
return this.#request(
|
|
145
|
+
"POST",
|
|
146
|
+
`/v1/entity/${entity}/find${q}`,
|
|
147
|
+
conditions ?? {},
|
|
148
|
+
);
|
|
105
149
|
}
|
|
106
150
|
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
151
|
+
/**
|
|
152
|
+
* 목록 조회 (POST + conditions body)
|
|
153
|
+
*
|
|
154
|
+
* @param {Object} [opts]
|
|
155
|
+
* @param {number} [opts.page] 페이지 번호 (기본 1)
|
|
156
|
+
* @param {number} [opts.limit] 페이지당 건수 (기본 20, 최대 1000)
|
|
157
|
+
* @param {string} [opts.orderBy] 정렬 기준 필드명 (- 접두사로 내림차순)
|
|
158
|
+
* @param {string} [opts.orderDir] 정렬 방향 ('ASC'|'DESC')
|
|
159
|
+
* @param {string[]} [opts.fields] 반환 필드 목록. 미지정 시 인덱스 필드만 반환 (기본, 가장 빠름). '*' 지정 시 전체 필드 반환
|
|
160
|
+
* @param {Object} [opts.conditions] 필터 조건. index/hash/unique 필드만 사용 가능
|
|
161
|
+
*/
|
|
162
|
+
list(
|
|
163
|
+
entity,
|
|
164
|
+
{ page = 1, limit = 20, orderBy, orderDir, fields, conditions } = {},
|
|
165
|
+
) {
|
|
166
|
+
const qParams = { page, limit };
|
|
167
|
+
if (orderBy)
|
|
168
|
+
qParams.order_by = orderDir === "DESC" ? `-${orderBy}` : orderBy;
|
|
169
|
+
if (fields?.length) qParams.fields = fields.join(",");
|
|
170
|
+
const q = new URLSearchParams(qParams);
|
|
171
|
+
return this.#request(
|
|
172
|
+
"POST",
|
|
173
|
+
`/v1/entity/${entity}/list?${q}`,
|
|
174
|
+
conditions ?? {},
|
|
175
|
+
);
|
|
110
176
|
}
|
|
111
177
|
|
|
112
178
|
/**
|
|
113
|
-
*
|
|
114
|
-
* @param {
|
|
115
|
-
* @param {Object} params 예: { page: 1, limit: 20, orderBy: 'name' }
|
|
179
|
+
* 건수 조회
|
|
180
|
+
* @param {Object} [conditions] 필터 조건 (list() 와 동일 규칙)
|
|
116
181
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
182
|
+
count(entity, conditions) {
|
|
183
|
+
return this.#request(
|
|
184
|
+
"POST",
|
|
185
|
+
`/v1/entity/${entity}/count`,
|
|
186
|
+
conditions ?? {},
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 커스텀 SQL 조회 (SELECT 전용, 인덱스 테이블만, JOIN 지원)
|
|
192
|
+
*
|
|
193
|
+
* @param {Object} req
|
|
194
|
+
* @param {string} req.sql SELECT SQL문. 사용자 입력은 반드시 params 로 바인딩 (SQL Injection 방지)
|
|
195
|
+
* @param {Array} [req.params] ? 플레이스홀더 바인딩 값
|
|
196
|
+
* @param {number} [req.limit] 최대 반환 건수 (최대 1000)
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* es.query('order', {
|
|
200
|
+
* sql: 'SELECT o.seq, u.name FROM order o JOIN account u ON u.data_seq = o.account_seq WHERE o.status = ?',
|
|
201
|
+
* params: ['pending'],
|
|
202
|
+
* limit: 100,
|
|
203
|
+
* });
|
|
204
|
+
*/
|
|
205
|
+
query(entity, { sql, params = [], limit } = {}) {
|
|
206
|
+
const body = { sql, params };
|
|
207
|
+
if (limit != null) body.limit = limit;
|
|
208
|
+
return this.#request("POST", `/v1/entity/${entity}/query`, body);
|
|
124
209
|
}
|
|
125
210
|
|
|
126
211
|
/**
|
|
@@ -131,12 +216,13 @@ export class EntityServerClient {
|
|
|
131
216
|
* @param {Object} [opts]
|
|
132
217
|
* @param {string} [opts.transactionId] transStart() 가 반환한 ID (생략 시 활성 트랜잭션 자동 사용)
|
|
133
218
|
*/
|
|
134
|
-
submit(entity, data, { transactionId } = {}) {
|
|
219
|
+
submit(entity, data, { transactionId, skipHooks = false } = {}) {
|
|
135
220
|
const txId = transactionId ?? this.#activeTxId;
|
|
136
221
|
const extra = txId ? { "X-Transaction-ID": txId } : {};
|
|
222
|
+
const q = skipHooks ? "?skipHooks=true" : "";
|
|
137
223
|
return this.#request(
|
|
138
224
|
"POST",
|
|
139
|
-
`/v1/entity/${entity}/submit`,
|
|
225
|
+
`/v1/entity/${entity}/submit${q}`,
|
|
140
226
|
data,
|
|
141
227
|
extra,
|
|
142
228
|
);
|
|
@@ -150,12 +236,19 @@ export class EntityServerClient {
|
|
|
150
236
|
* @param {string} [opts.transactionId] transStart() 가 반환한 ID (생략 시 활성 트랜잭션 자동 사용)
|
|
151
237
|
* @param {boolean} [opts.hard] 하드 삭제 여부 (기본 false)
|
|
152
238
|
*/
|
|
153
|
-
delete(
|
|
154
|
-
|
|
239
|
+
delete(
|
|
240
|
+
entity,
|
|
241
|
+
seq,
|
|
242
|
+
{ transactionId, hard = false, skipHooks = false } = {},
|
|
243
|
+
) {
|
|
244
|
+
const params = new URLSearchParams();
|
|
245
|
+
if (hard) params.set("hard", "true");
|
|
246
|
+
if (skipHooks) params.set("skipHooks", "true");
|
|
247
|
+
const q = params.size ? `?${params}` : "";
|
|
155
248
|
const txId = transactionId ?? this.#activeTxId;
|
|
156
249
|
const extra = txId ? { "X-Transaction-ID": txId } : {};
|
|
157
250
|
return this.#request(
|
|
158
|
-
"
|
|
251
|
+
"POST",
|
|
159
252
|
`/v1/entity/${entity}/delete/${seq}${q}`,
|
|
160
253
|
null,
|
|
161
254
|
extra,
|
|
@@ -297,24 +390,51 @@ export class EntityServerClient {
|
|
|
297
390
|
// ─── 내부 ─────────────────────────────────────────────────────────────────
|
|
298
391
|
|
|
299
392
|
async #request(method, path, body, extraHeaders = {}) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
393
|
+
// 요청 바디 결정: encryptRequests 시 POST 바디를 암호화합니다.
|
|
394
|
+
let bodyData = null; // string | Buffer | null
|
|
395
|
+
if (body != null) {
|
|
396
|
+
if (this.#encryptRequests || this.#packetEncryption) {
|
|
397
|
+
const plaintext = Buffer.from(JSON.stringify(body));
|
|
398
|
+
bodyData = this.#encryptPacket(plaintext); // Buffer
|
|
399
|
+
} else {
|
|
400
|
+
bodyData = JSON.stringify(body);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const isHmacMode = !!(this.#apiKey && this.#hmacSecret);
|
|
405
|
+
|
|
406
|
+
const contentType =
|
|
407
|
+
(this.#encryptRequests || this.#packetEncryption) &&
|
|
408
|
+
bodyData instanceof Buffer
|
|
409
|
+
? "application/octet-stream"
|
|
410
|
+
: "application/json";
|
|
411
|
+
|
|
412
|
+
const headers = { "Content-Type": contentType };
|
|
413
|
+
|
|
414
|
+
if (isHmacMode) {
|
|
415
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
416
|
+
const nonce = randomUUID();
|
|
417
|
+
const signature = this.#sign(
|
|
418
|
+
method,
|
|
419
|
+
path,
|
|
420
|
+
timestamp,
|
|
421
|
+
nonce,
|
|
422
|
+
bodyData ?? "",
|
|
423
|
+
);
|
|
424
|
+
headers["X-API-Key"] = this.#apiKey;
|
|
425
|
+
headers["X-Timestamp"] = timestamp;
|
|
426
|
+
headers["X-Nonce"] = nonce;
|
|
427
|
+
headers["X-Signature"] = signature;
|
|
428
|
+
} else if (this.#token) {
|
|
429
|
+
headers["Authorization"] = `Bearer ${this.#token}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
Object.assign(headers, extraHeaders);
|
|
313
433
|
|
|
314
434
|
const res = await fetch(this.#baseUrl + path, {
|
|
315
435
|
method,
|
|
316
436
|
headers,
|
|
317
|
-
...(
|
|
437
|
+
...(bodyData != null ? { body: bodyData } : {}),
|
|
318
438
|
});
|
|
319
439
|
|
|
320
440
|
const contentType = res.headers.get("Content-Type") ?? "";
|
|
@@ -335,27 +455,80 @@ export class EntityServerClient {
|
|
|
335
455
|
return data;
|
|
336
456
|
}
|
|
337
457
|
|
|
458
|
+
/**
|
|
459
|
+
* 패킷 암호화 키를 유도합니다.
|
|
460
|
+
* - HMAC 모드: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
461
|
+
* - JWT 모드: SHA256(token)
|
|
462
|
+
*/
|
|
463
|
+
#derivePacketKey() {
|
|
464
|
+
if (this.#token && !this.#hmacSecret) {
|
|
465
|
+
return sha256(new TextEncoder().encode(this.#token));
|
|
466
|
+
}
|
|
467
|
+
const salt = new TextEncoder().encode("entity-server:hkdf:v1");
|
|
468
|
+
const info = new TextEncoder().encode(
|
|
469
|
+
"entity-server:packet-encryption",
|
|
470
|
+
);
|
|
471
|
+
return hkdf(
|
|
472
|
+
sha256,
|
|
473
|
+
new TextEncoder().encode(this.#hmacSecret),
|
|
474
|
+
salt,
|
|
475
|
+
info,
|
|
476
|
+
32,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* XChaCha20-Poly1305 패킷 암호화
|
|
482
|
+
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
483
|
+
* magicLen: 패킷 키의 마지막 바이트에서 파생 (2 + key[31] % 14)
|
|
484
|
+
* @param {Buffer} plaintext
|
|
485
|
+
* @returns {Buffer}
|
|
486
|
+
*/
|
|
487
|
+
#encryptPacket(plaintext) {
|
|
488
|
+
const key = this.#derivePacketKey();
|
|
489
|
+
const magicLen = 2 + (key[31] % 14);
|
|
490
|
+
const magic = Buffer.allocUnsafe(magicLen);
|
|
491
|
+
const nonce = Buffer.allocUnsafe(24);
|
|
492
|
+
randomFillSync(magic);
|
|
493
|
+
randomFillSync(nonce);
|
|
494
|
+
const cipher = xchacha20_poly1305(key, nonce);
|
|
495
|
+
const ciphertext = cipher.encrypt(
|
|
496
|
+
plaintext instanceof Uint8Array
|
|
497
|
+
? plaintext
|
|
498
|
+
: new Uint8Array(plaintext),
|
|
499
|
+
);
|
|
500
|
+
return Buffer.concat([magic, nonce, Buffer.from(ciphertext)]);
|
|
501
|
+
}
|
|
502
|
+
|
|
338
503
|
/**
|
|
339
504
|
* XChaCha20-Poly1305 패킷 복호화
|
|
340
505
|
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
341
|
-
* 키:
|
|
506
|
+
* 키: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
342
507
|
*/
|
|
343
508
|
#decryptPacket(buffer) {
|
|
344
|
-
const key =
|
|
509
|
+
const key = this.#derivePacketKey();
|
|
510
|
+
const magicLen = 2 + (key[31] % 14);
|
|
345
511
|
const data = new Uint8Array(buffer);
|
|
346
|
-
const nonce = data.slice(
|
|
347
|
-
const ciphertext = data.slice(
|
|
512
|
+
const nonce = data.slice(magicLen, magicLen + 24);
|
|
513
|
+
const ciphertext = data.slice(magicLen + 24);
|
|
348
514
|
const cipher = xchacha20_poly1305(key, nonce);
|
|
349
515
|
const plaintext = cipher.decrypt(ciphertext);
|
|
350
516
|
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
351
517
|
}
|
|
352
518
|
|
|
353
|
-
/**
|
|
519
|
+
/**
|
|
520
|
+
* HMAC-SHA256 서명
|
|
521
|
+
* body 는 문자열(JSON) 또는 Buffer(암호화된 바디) 모두 지원합니다.
|
|
522
|
+
*/
|
|
354
523
|
#sign(method, path, timestamp, nonce, body) {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
524
|
+
const mac = createHmac("sha256", this.#hmacSecret);
|
|
525
|
+
const prefix = `${method}|${path}|${timestamp}|${nonce}|`;
|
|
526
|
+
mac.update(prefix);
|
|
527
|
+
if (body != null && body !== "") {
|
|
528
|
+
// Buffer(binary) 또는 string 모두 처리 — Go 서버의 string(c.Body()) 와 동일한 바이트
|
|
529
|
+
mac.update(typeof body === "string" ? body : Buffer.from(body));
|
|
530
|
+
}
|
|
531
|
+
return mac.digest("hex");
|
|
359
532
|
}
|
|
360
533
|
}
|
|
361
534
|
|
|
@@ -8,7 +8,7 @@ const es = new EntityServerClient({
|
|
|
8
8
|
|
|
9
9
|
// 목록 조회
|
|
10
10
|
const list = await es.list("product", { page: 1, limit: 10 });
|
|
11
|
-
console.log("List:", list.data?.
|
|
11
|
+
console.log("List:", list.data?.total, "items");
|
|
12
12
|
|
|
13
13
|
// 생성
|
|
14
14
|
const created = await es.submit("product", {
|
|
@@ -22,17 +22,17 @@ console.log("Created seq:", created.seq);
|
|
|
22
22
|
await es.submit("product", { seq: created.seq, price: 79000 });
|
|
23
23
|
console.log("Updated");
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
const results = await es.query(
|
|
27
|
-
"product",
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
);
|
|
31
|
-
console.log("Query results:", results.data?.length);
|
|
25
|
+
// 커스텀 SQL 검색
|
|
26
|
+
const results = await es.query("product", {
|
|
27
|
+
sql: "SELECT seq, name, category FROM product WHERE category = ?",
|
|
28
|
+
params: ["peripherals"],
|
|
29
|
+
limit: 5,
|
|
30
|
+
});
|
|
31
|
+
console.log("Query results:", results.data?.items?.length);
|
|
32
32
|
|
|
33
33
|
// 이력 조회
|
|
34
34
|
const hist = await es.history("product", created.seq);
|
|
35
|
-
console.log("History count:", hist.data?.length);
|
|
35
|
+
console.log("History count:", hist.data?.items?.length);
|
|
36
36
|
|
|
37
37
|
// 삭제
|
|
38
38
|
await es.delete("product", created.seq);
|