create-entity-server 0.0.15 → 0.0.25

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.
Files changed (42) hide show
  1. package/bin/create.js +15 -7
  2. package/package.json +1 -1
  3. package/template/.env.example +8 -7
  4. package/template/configs/database.json +173 -10
  5. package/template/entities/Account/account_audit.json +4 -5
  6. package/template/entities/System/system_audit_log.json +14 -8
  7. package/template/samples/README.md +28 -22
  8. package/template/samples/browser/entity-server-client.js +453 -0
  9. package/template/samples/browser/example.html +498 -0
  10. package/template/samples/entities/02_types_and_defaults.json +15 -16
  11. package/template/samples/entities/04_fk_and_composite_unique.json +0 -2
  12. package/template/samples/entities/05_cache.json +9 -8
  13. package/template/samples/entities/06_history_and_hard_delete.json +27 -9
  14. package/template/samples/entities/07_license_scope.json +40 -31
  15. package/template/samples/entities/09_hook_entity.json +0 -6
  16. package/template/samples/entities/10_hook_submit_delete.json +5 -2
  17. package/template/samples/entities/11_hook_webhook.json +9 -7
  18. package/template/samples/entities/12_hook_push.json +3 -3
  19. package/template/samples/entities/13_read_only.json +13 -10
  20. package/template/samples/entities/15_reset_defaults.json +0 -1
  21. package/template/samples/entities/16_isolated_license.json +62 -0
  22. package/template/samples/entities/README.md +36 -39
  23. package/template/samples/flutter/lib/entity_server_client.dart +170 -48
  24. package/template/samples/java/EntityServerClient.java +208 -61
  25. package/template/samples/java/EntityServerExample.java +4 -3
  26. package/template/samples/kotlin/EntityServerClient.kt +175 -45
  27. package/template/samples/node/src/EntityServerClient.js +232 -59
  28. package/template/samples/node/src/example.js +9 -9
  29. package/template/samples/php/ci4/Config/EntityServer.php +0 -1
  30. package/template/samples/php/ci4/Libraries/EntityServer.php +206 -53
  31. package/template/samples/php/laravel/Services/EntityServerService.php +190 -41
  32. package/template/samples/python/entity_server.py +181 -68
  33. package/template/samples/python/example.py +7 -6
  34. package/template/samples/react/src/example.tsx +41 -25
  35. package/template/samples/swift/EntityServerClient.swift +143 -37
  36. package/template/scripts/run.ps1 +12 -3
  37. package/template/scripts/run.sh +12 -8
  38. package/template/scripts/update-server.ps1 +68 -2
  39. package/template/scripts/update-server.sh +59 -2
  40. package/template/samples/entities/order_notification.json +0 -51
  41. package/template/samples/react/src/api/entityServerClient.ts +0 -413
  42. package/template/samples/react/src/hooks/useEntity.ts +0 -173
@@ -1,8 +1,11 @@
1
1
  package com.example.entityserver;
2
2
 
3
+ import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
3
4
  import org.bouncycastle.crypto.modes.ChaCha20Poly1305;
4
5
  import org.bouncycastle.crypto.params.AEADParameters;
6
+ import org.bouncycastle.crypto.params.HKDFParameters;
5
7
  import org.bouncycastle.crypto.params.KeyParameter;
8
+ import org.bouncycastle.crypto.digests.SHA256Digest;
6
9
 
7
10
  import javax.crypto.Mac;
8
11
  import javax.crypto.spec.SecretKeySpec;
@@ -14,6 +17,7 @@ import java.net.HttpURLConnection;
14
17
  import java.net.URL;
15
18
  import java.nio.charset.StandardCharsets;
16
19
  import java.security.MessageDigest;
20
+ import java.security.SecureRandom;
17
21
  import java.util.Arrays;
18
22
  import java.util.Collections;
19
23
  import java.util.HashMap;
@@ -29,9 +33,9 @@ import java.util.UUID;
29
33
  *
30
34
  * 환경변수 또는 생성자로 설정:
31
35
  * ENTITY_SERVER_URL http://localhost:47200
32
- * ENTITY_SERVER_API_KEY your-api-key
33
- * ENTITY_SERVER_HMAC_SECRET your-hmac-secret
34
- * ENTITY_PACKET_MAGIC_LEN 4 (서버 packet_magic_len 과 동일)
36
+ * ENTITY_SERVER_API_KEY your-api-key (HMAC 모드)
37
+ * ENTITY_SERVER_HMAC_SECRET your-hmac-secret (HMAC 모드)
38
+ * ENTITY_SERVER_TOKEN your-jwt-token (JWT 모드)
35
39
  *
36
40
  * 사용 예:
37
41
  * EntityServerClient es = new EntityServerClient();
@@ -55,12 +59,15 @@ import java.util.UUID;
55
59
  */
56
60
  public class EntityServerClient {
57
61
 
58
- private final String baseUrl;
59
- private final String apiKey;
60
- private final String hmacSecret;
61
- private final int timeoutMs;
62
- private final int magicLen;
63
- private String activeTxId = null;
62
+ private final String baseUrl;
63
+ private final String apiKey;
64
+ private final String hmacSecret;
65
+ private final int timeoutMs;
66
+ /** true 이면 POST 요청 바디를 XChaCha20-Poly1305로 암호화합니다. */
67
+ private boolean encryptRequests;
68
+ private boolean packetEncryption = false;
69
+ private String token = "";
70
+ private String activeTxId = null;
64
71
 
65
72
  public EntityServerClient() {
66
73
  this(
@@ -68,43 +75,100 @@ public class EntityServerClient {
68
75
  getEnv("ENTITY_SERVER_API_KEY", ""),
69
76
  getEnv("ENTITY_SERVER_HMAC_SECRET", ""),
70
77
  10_000,
71
- Integer.parseInt(getEnv("ENTITY_PACKET_MAGIC_LEN", "4"))
78
+ false
72
79
  );
80
+ this.token = getEnv("ENTITY_SERVER_TOKEN", "");
73
81
  }
74
82
 
75
- public EntityServerClient(String baseUrl, String apiKey, String hmacSecret, int timeoutMs, int magicLen) {
76
- this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
77
- this.apiKey = apiKey;
78
- this.hmacSecret = hmacSecret;
79
- this.timeoutMs = timeoutMs;
80
- this.magicLen = magicLen;
83
+ /** JWT Bearer 토큰을 설정합니다. HMAC 모드와 배타적으로 사용합니다. */
84
+ public void setToken(String token) { this.token = token; }
85
+
86
+ public EntityServerClient(String baseUrl, String apiKey, String hmacSecret, int timeoutMs) {
87
+ this(baseUrl, apiKey, hmacSecret, timeoutMs, false);
81
88
  }
82
89
 
83
- // ─── CRUD ────────────────────────────────────────────────────────────────
90
+ public EntityServerClient(String baseUrl, String apiKey, String hmacSecret, int timeoutMs, boolean encryptRequests) {
91
+ this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
92
+ this.apiKey = apiKey;
93
+ this.hmacSecret = hmacSecret;
94
+ this.timeoutMs = timeoutMs;
95
+ this.encryptRequests = encryptRequests;
96
+ }
84
97
 
98
+ // ─── CRUD ────────────────────────────────────────────────────────────────
99
+ /**
100
+ * 서버 헬스 체크를 수행하고 패킷 암호화 활성 여부를 자동으로 감지합니다.
101
+ * 서버가 packet_encryption: true 를 응답하면 이후 모든 요청에 암호화가 자동 적용됩니다.
102
+ * @return JSON 문자열 (예: {"ok":true} 또는 {"ok":true,"packet_encryption":true})
103
+ */
104
+ public String checkHealth() throws IOException {
105
+ String json = request("GET", "/v1/health", null);
106
+ if (json != null && json.contains("\"packet_encryption\":true")) {
107
+ packetEncryption = true;
108
+ }
109
+ return json;
110
+ }
85
111
  /** 단건 조회 */
86
112
  public String get(String entity, long seq) throws IOException {
87
- return request("GET", "/v1/entity/" + entity + "/" + seq, null);
113
+ return get(entity, seq, false);
88
114
  }
89
115
 
90
- /** 목록 조회 */
91
- public String list(String entity, int page, int limit, String orderBy) throws IOException {
116
+ /** 단건 조회 (skipHooks 지원) */
117
+ public String get(String entity, long seq, boolean skipHooks) throws IOException {
118
+ String q = skipHooks ? "?skipHooks=true" : "";
119
+ return request("GET", "/v1/entity/" + entity + "/" + seq + q, null);
120
+ }
121
+
122
+ /** 조건으로 단건 조회 — conditions 에 맞는 행 1건 반환, 없으면 404 */
123
+ public String find(String entity, String conditionsJson) throws IOException {
124
+ return find(entity, conditionsJson, false);
125
+ }
126
+
127
+ /** 조건으로 단건 조회 (skipHooks 지원) */
128
+ public String find(String entity, String conditionsJson, boolean skipHooks) throws IOException {
129
+ String q = skipHooks ? "?skipHooks=true" : "";
130
+ return request("POST", "/v1/entity/" + entity + "/find" + q,
131
+ conditionsJson != null ? conditionsJson : "{}");
132
+ }
133
+
134
+ /**
135
+ * 목록 조회 (POST + conditions body)
136
+ * @param orderBy 정렬 기준 필드명. null 이면 기본 정렬. - 접두사로 내림차순
137
+ * @param conditionsJson 필터 조건 JSON 객체. index/hash/unique 필드만 사용 가능. null 이면 전체
138
+ */
139
+ public String list(String entity, int page, int limit, String orderBy, String conditionsJson) throws IOException {
92
140
  String query = "?page=" + page + "&limit=" + limit + (orderBy != null ? "&order_by=" + orderBy : "");
93
- return request("GET", "/v1/entity/" + entity + "/list" + query, null);
141
+ return request("POST", "/v1/entity/" + entity + "/list" + query, conditionsJson != null ? conditionsJson : "{}");
142
+ }
143
+
144
+ /** 목록 조회 (조건 없음) */
145
+ public String list(String entity, int page, int limit, String orderBy) throws IOException {
146
+ return list(entity, page, limit, orderBy, null);
94
147
  }
95
148
 
96
149
  /** 건수 조회 */
97
150
  public String count(String entity) throws IOException {
98
- return request("GET", "/v1/entity/" + entity + "/count", null);
151
+ return count(entity, null);
152
+ }
153
+
154
+ /** 건수 조회 (conditions 지원) */
155
+ public String count(String entity, String conditionsJson) throws IOException {
156
+ return request("POST", "/v1/entity/" + entity + "/count", conditionsJson != null ? conditionsJson : "{}");
99
157
  }
100
158
 
101
159
  /**
102
- * 필터 검색
103
- * @param filterJson JSON 배열 문자열. 예: [{"field":"status","op":"eq","value":"active"}]
160
+ * 커스텀 SQL 조회 (SELECT 전용, 인덱스 테이블만, JOIN 지원)
161
+ *
162
+ * @param sql SELECT SQL문. 사용자 입력은 반드시 ? 로 바인딩 (SQL Injection 방지)
163
+ * @param paramsJson 바인딩 파라미터 JSON 배열. 예: "[\"pending\"]" (null 이면 빈 배열)
164
+ * @param limit 최대 반환 건수 (최대 1000. 0 이하이면 서버 기본값 적용)
104
165
  */
105
- public String query(String entity, String filterJson, int page, int limit) throws IOException {
106
- String query = "?page=" + page + "&limit=" + limit;
107
- return request("POST", "/v1/entity/" + entity + "/query" + query, filterJson);
166
+ public String query(String entity, String sql, String paramsJson, int limit) throws IOException {
167
+ StringBuilder body = new StringBuilder("{\"sql\":").append(jsonString(sql));
168
+ body.append(",\"params\":").append(paramsJson != null ? paramsJson : "[]");
169
+ if (limit > 0) body.append(",\"limit\":").append(limit);
170
+ body.append("}");
171
+ return request("POST", "/v1/entity/" + entity + "/query", body.toString());
108
172
  }
109
173
 
110
174
  /**
@@ -159,29 +223,42 @@ public class EntityServerClient {
159
223
  * @param dataJson JSON 객체 문자열. seq 포함 시 수정, 없으면 생성.
160
224
  */
161
225
  public String submit(String entity, String dataJson) throws IOException {
162
- return submit(entity, dataJson, null);
226
+ return submit(entity, dataJson, null, false);
163
227
  }
164
228
 
165
229
  /** 생성 또는 수정 (트랜잭션 지원) */
166
230
  public String submit(String entity, String dataJson, String transactionId) throws IOException {
231
+ return submit(entity, dataJson, transactionId, false);
232
+ }
233
+
234
+ /** 생성 또는 수정 (skipHooks 지원) */
235
+ public String submit(String entity, String dataJson, String transactionId, boolean skipHooks) throws IOException {
167
236
  Map<String, String> extra = new HashMap<>();
168
237
  String txId = transactionId != null ? transactionId : activeTxId;
169
238
  if (txId != null) extra.put("X-Transaction-ID", txId);
170
- return request("POST", "/v1/entity/" + entity + "/submit", dataJson, extra);
239
+ String q = skipHooks ? "?skipHooks=true" : "";
240
+ return request("POST", "/v1/entity/" + entity + "/submit" + q, dataJson, extra);
171
241
  }
172
242
 
173
243
  /** 삭제 */
174
244
  public String delete(String entity, long seq) throws IOException {
175
- return delete(entity, seq, null, false);
245
+ return delete(entity, seq, null, false, false);
176
246
  }
177
247
 
178
- /** 삭제 (트랜잭션 지원) */
248
+ /** 삭제 (트랜잭션/하드 삭제 지원) */
179
249
  public String delete(String entity, long seq, String transactionId, boolean hard) throws IOException {
180
- String q = hard ? "?hard=true" : "";
250
+ return delete(entity, seq, transactionId, hard, false);
251
+ }
252
+
253
+ /** 삭제 (skipHooks 지원). 서버는 POST /delete/:seq 로만 처리합니다. */
254
+ public String delete(String entity, long seq, String transactionId, boolean hard, boolean skipHooks) throws IOException {
255
+ StringBuilder qb = new StringBuilder();
256
+ if (hard) { qb.append(qb.length() == 0 ? "?" : "&").append("hard=true"); }
257
+ if (skipHooks) { qb.append(qb.length() == 0 ? "?" : "&").append("skipHooks=true"); }
181
258
  Map<String, String> extra = new HashMap<>();
182
259
  String txId = transactionId != null ? transactionId : activeTxId;
183
260
  if (txId != null) extra.put("X-Transaction-ID", txId);
184
- return request("DELETE", "/v1/entity/" + entity + "/delete/" + seq + q, null, extra);
261
+ return request("POST", "/v1/entity/" + entity + "/delete/" + seq + qb.toString(), null, extra);
185
262
  }
186
263
 
187
264
  /** 변경 이력 조회 */
@@ -306,40 +383,59 @@ public class EntityServerClient {
306
383
  }
307
384
 
308
385
  private String request(String method, String path, String body, Map<String, String> extraHeaders) throws IOException {
309
- String bodyStr = body != null ? body : "";
310
- String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
311
- String nonce = UUID.randomUUID().toString();
312
- String signature = sign(method, path, timestamp, nonce, bodyStr);
386
+ // 요청 바디 결정: encryptRequests POST 바디를 암호화
387
+ byte[] bodyBytes;
388
+ String contentType;
389
+ if ((encryptRequests || packetEncryption) && body != null && !body.isEmpty()) {
390
+ try {
391
+ bodyBytes = encryptPacket(body.getBytes(StandardCharsets.UTF_8));
392
+ } catch (Exception e) {
393
+ throw new IOException("Packet encryption failed: " + e.getMessage(), e);
394
+ }
395
+ contentType = "application/octet-stream";
396
+ } else {
397
+ bodyBytes = body != null ? body.getBytes(StandardCharsets.UTF_8) : new byte[0];
398
+ contentType = "application/json";
399
+ }
400
+
401
+ boolean isHmacMode = !apiKey.isEmpty() && !hmacSecret.isEmpty();
313
402
 
314
403
  URL url = new URL(baseUrl + path);
315
404
  HttpURLConnection conn = (HttpURLConnection) url.openConnection();
316
405
  conn.setRequestMethod(method);
317
406
  conn.setConnectTimeout(timeoutMs);
318
407
  conn.setReadTimeout(timeoutMs);
319
- conn.setRequestProperty("Content-Type", "application/json");
320
- conn.setRequestProperty("X-API-Key", apiKey);
321
- conn.setRequestProperty("X-Timestamp", timestamp);
322
- conn.setRequestProperty("X-Nonce", nonce);
323
- conn.setRequestProperty("X-Signature", signature);
408
+ conn.setRequestProperty("Content-Type", contentType);
409
+ if (isHmacMode) {
410
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
411
+ String nonce = UUID.randomUUID().toString();
412
+ String signature = sign(method, path, timestamp, nonce, bodyBytes);
413
+ conn.setRequestProperty("X-API-Key", apiKey);
414
+ conn.setRequestProperty("X-Timestamp", timestamp);
415
+ conn.setRequestProperty("X-Nonce", nonce);
416
+ conn.setRequestProperty("X-Signature", signature);
417
+ } else if (token != null && !token.isEmpty()) {
418
+ conn.setRequestProperty("Authorization", "Bearer " + token);
419
+ }
324
420
  for (Map.Entry<String, String> h : extraHeaders.entrySet()) {
325
421
  conn.setRequestProperty(h.getKey(), h.getValue());
326
422
  }
327
423
 
328
- if (!bodyStr.isEmpty()) {
424
+ if (bodyBytes.length > 0) {
329
425
  conn.setDoOutput(true);
330
426
  try (OutputStream os = conn.getOutputStream()) {
331
- os.write(bodyStr.getBytes(StandardCharsets.UTF_8));
427
+ os.write(bodyBytes);
332
428
  }
333
429
  }
334
430
 
335
431
  int status = conn.getResponseCode();
336
- String contentType = conn.getContentType();
432
+ String respCT = conn.getContentType();
337
433
  InputStream stream = status >= 400 ? conn.getErrorStream() : conn.getInputStream();
338
434
  byte[] rawBytes = readAllBytes(stream);
339
435
 
340
436
  // 패킷 암호화 응답: application/octet-stream → 복호화
341
437
  String response;
342
- if (contentType != null && contentType.contains("application/octet-stream")) {
438
+ if (respCT != null && respCT.contains("application/octet-stream")) {
343
439
  try {
344
440
  response = decryptPacket(rawBytes);
345
441
  } catch (Exception e) {
@@ -355,17 +451,68 @@ public class EntityServerClient {
355
451
  return response;
356
452
  }
357
453
 
454
+ /**
455
+ * 패킷 암호화 키를 유도합니다.
456
+ * - HMAC 모드: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
457
+ * - JWT 모드: SHA256(token)
458
+ */
459
+ private byte[] derivePacketKey() {
460
+ if (token != null && !token.isEmpty() && hmacSecret.isEmpty()) {
461
+ try {
462
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
463
+ return digest.digest(token.getBytes(StandardCharsets.UTF_8));
464
+ } catch (Exception e) {
465
+ throw new RuntimeException("SHA-256 failed", e);
466
+ }
467
+ }
468
+ HKDFBytesGenerator gen = new HKDFBytesGenerator(new SHA256Digest());
469
+ gen.init(new HKDFParameters(
470
+ hmacSecret.getBytes(StandardCharsets.UTF_8),
471
+ "entity-server:hkdf:v1".getBytes(StandardCharsets.UTF_8),
472
+ "entity-server:packet-encryption".getBytes(StandardCharsets.UTF_8)
473
+ ));
474
+ byte[] key = new byte[32];
475
+ gen.generateBytes(key, 0, 32);
476
+ return key;
477
+ }
478
+
479
+ /**
480
+ * XChaCha20-Poly1305 패킷 암호화
481
+ * 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
482
+ * magicLen: 2 + (key[31] & 0xFF) % 14
483
+ */
484
+ private byte[] encryptPacket(byte[] plaintext) throws Exception {
485
+ byte[] key = derivePacketKey();
486
+ int magicLen = 2 + (key[31] & 0xFF) % 14;
487
+ byte[] magic = new byte[magicLen];
488
+ byte[] nonce = new byte[24];
489
+ SecureRandom rng = new SecureRandom();
490
+ rng.nextBytes(magic);
491
+ rng.nextBytes(nonce);
492
+
493
+ ChaCha20Poly1305 aead = new ChaCha20Poly1305();
494
+ aead.init(true, new AEADParameters(new KeyParameter(key), 128, nonce));
495
+ byte[] ciphertext = new byte[aead.getOutputSize(plaintext.length)];
496
+ int len = aead.processBytes(plaintext, 0, plaintext.length, ciphertext, 0);
497
+ aead.doFinal(ciphertext, len);
498
+
499
+ byte[] result = new byte[magic.length + nonce.length + ciphertext.length];
500
+ System.arraycopy(magic, 0, result, 0, magic.length);
501
+ System.arraycopy(nonce, 0, result, magic.length, nonce.length);
502
+ System.arraycopy(ciphertext, 0, result, magic.length + nonce.length, ciphertext.length);
503
+ return result;
504
+ }
505
+
358
506
  /**
359
507
  * XChaCha20-Poly1305 패킷 복호화
360
508
  * 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
361
- * 키: sha256(hmac_secret)
362
- *
363
- * Bouncy Castle ChaCha20Poly1305 사용 (nonce 24바이트 → 자동으로 XChaCha20 선택)
509
+ * 키: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
364
510
  */
365
511
  private String decryptPacket(byte[] data) throws Exception {
366
- byte[] key = sha256(hmacSecret.getBytes(StandardCharsets.UTF_8));
367
- byte[] nonce = Arrays.copyOfRange(data, magicLen, magicLen + 24);
368
- byte[] ctext = Arrays.copyOfRange(data, magicLen + 24, data.length);
512
+ byte[] key = derivePacketKey();
513
+ int magicLen = 2 + (key[31] & 0xFF) % 14;
514
+ byte[] nonce = Arrays.copyOfRange(data, magicLen, magicLen + 24);
515
+ byte[] ctext = Arrays.copyOfRange(data, magicLen + 24, data.length);
369
516
 
370
517
  ChaCha20Poly1305 aead = new ChaCha20Poly1305();
371
518
  aead.init(false, new AEADParameters(new KeyParameter(key), 128, nonce));
@@ -376,23 +523,23 @@ public class EntityServerClient {
376
523
  return new String(plaintext, StandardCharsets.UTF_8);
377
524
  }
378
525
 
379
- /** HMAC-SHA256 서명 */
380
- private String sign(String method, String path, String timestamp, String nonce, String body) {
526
+ /**
527
+ * HMAC-SHA256 서명. bodyBytes JSON 또는 암호화된 바이너리 모두 지원합니다.
528
+ */
529
+ private String sign(String method, String path, String timestamp, String nonce, byte[] bodyBytes) {
381
530
  try {
382
- String payload = String.join("|", method, path, timestamp, nonce, body);
383
531
  Mac mac = Mac.getInstance("HmacSHA256");
384
532
  mac.init(new SecretKeySpec(hmacSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
385
- byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
386
- return HexFormat.of().formatHex(hash); // Java 17+
533
+ String prefix = method + "|" + path + "|" + timestamp + "|" + nonce + "|";
534
+ mac.update(prefix.getBytes(StandardCharsets.UTF_8));
535
+ if (bodyBytes != null && bodyBytes.length > 0) mac.update(bodyBytes);
536
+ byte[] hash = mac.doFinal();
537
+ return HexFormat.of().formatHex(hash);
387
538
  } catch (Exception e) {
388
539
  throw new RuntimeException("HMAC signing failed", e);
389
540
  }
390
541
  }
391
542
 
392
- private static byte[] sha256(byte[] input) throws Exception {
393
- return MessageDigest.getInstance("SHA-256").digest(input);
394
- }
395
-
396
543
  private static byte[] readAllBytes(InputStream in) throws IOException {
397
544
  ByteArrayOutputStream buf = new ByteArrayOutputStream();
398
545
  byte[] chunk = new byte[8192];
@@ -31,10 +31,11 @@ public class EntityServerExample {
31
31
  String updated = es.submit("product", "{\"seq\":1,\"name\":\"게이밍 노트북\",\"price\":2000000}");
32
32
  System.out.println("Updated: " + updated);
33
33
 
34
- // 필터 검색
34
+ // 커스텀 SQL 검색
35
35
  String results = es.query("product",
36
- "[{\"field\":\"category\",\"op\":\"eq\",\"value\":\"electronics\"}]",
37
- 1, 10
36
+ "SELECT seq, name, category FROM product WHERE category = ?",
37
+ "[\"electronics\"]",
38
+ 10
38
39
  );
39
40
  System.out.println("Query: " + results);
40
41