lemon-core 4.2.7 → 5.0.0

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 (167) hide show
  1. package/README.md +128 -2
  2. package/dist/common/lemon-error.d.ts +34 -0
  3. package/dist/common/lemon-error.js +52 -0
  4. package/dist/common/lemon-error.js.map +1 -0
  5. package/dist/controllers/general-api-controller.d.ts +7 -0
  6. package/dist/controllers/general-api-controller.js +7 -0
  7. package/dist/controllers/general-api-controller.js.map +1 -1
  8. package/dist/cores/api/api-service.js +10 -43
  9. package/dist/cores/api/api-service.js.map +1 -1
  10. package/dist/cores/aws/aws-kms-service.d.ts +13 -5
  11. package/dist/cores/aws/aws-kms-service.js +23 -9
  12. package/dist/cores/aws/aws-kms-service.js.map +1 -1
  13. package/dist/cores/aws/aws-s3-service.d.ts +30 -4
  14. package/dist/cores/aws/aws-s3-service.js +35 -15
  15. package/dist/cores/aws/aws-s3-service.js.map +1 -1
  16. package/dist/cores/aws/aws-sns-service.js +59 -9
  17. package/dist/cores/aws/aws-sns-service.js.map +1 -1
  18. package/dist/cores/aws/aws-sqs-service.d.ts +1 -1
  19. package/dist/cores/aws/aws-sqs-service.js +54 -12
  20. package/dist/cores/aws/aws-sqs-service.js.map +1 -1
  21. package/dist/cores/cache/cache-service.d.ts +10 -9
  22. package/dist/cores/cache/cache-service.js +9 -346
  23. package/dist/cores/cache/cache-service.js.map +1 -1
  24. package/dist/cores/config/config-service.d.ts +3 -15
  25. package/dist/cores/config/config-service.js +5 -34
  26. package/dist/cores/config/config-service.js.map +1 -1
  27. package/dist/cores/dynamo/dynamo-query-service.js +42 -2
  28. package/dist/cores/dynamo/dynamo-query-service.js.map +1 -1
  29. package/dist/cores/dynamo/dynamo-scan-service.js +42 -2
  30. package/dist/cores/dynamo/dynamo-scan-service.js.map +1 -1
  31. package/dist/cores/dynamo/dynamo-service.d.ts +45 -14
  32. package/dist/cores/dynamo/dynamo-service.js +48 -21
  33. package/dist/cores/dynamo/dynamo-service.js.map +1 -1
  34. package/dist/cores/dynamo/tools/expressions.js +32 -22
  35. package/dist/cores/dynamo/tools/expressions.js.map +1 -1
  36. package/dist/cores/dynamo/tools/lang.d.ts +30 -0
  37. package/dist/cores/dynamo/tools/lang.js +75 -0
  38. package/dist/cores/dynamo/tools/lang.js.map +1 -0
  39. package/dist/cores/dynamo/tools/query.js +20 -21
  40. package/dist/cores/dynamo/tools/query.js.map +1 -1
  41. package/dist/cores/dynamo/tools/scan.js +15 -16
  42. package/dist/cores/dynamo/tools/scan.js.map +1 -1
  43. package/dist/cores/dynamo/tools/serializer.js +40 -38
  44. package/dist/cores/dynamo/tools/serializer.js.map +1 -1
  45. package/dist/cores/dynamo/tools/utils.d.ts +6 -2
  46. package/dist/cores/dynamo/tools/utils.js +11 -8
  47. package/dist/cores/dynamo/tools/utils.js.map +1 -1
  48. package/dist/cores/elastic/elastic6-query-service.d.ts +1 -104
  49. package/dist/cores/elastic/elastic6-query-service.js +15 -489
  50. package/dist/cores/elastic/elastic6-query-service.js.map +1 -1
  51. package/dist/cores/elastic/elastic6-service.d.ts +1 -482
  52. package/dist/cores/elastic/elastic6-service.js +15 -1153
  53. package/dist/cores/elastic/elastic6-service.js.map +1 -1
  54. package/dist/cores/elastic/index.d.ts +2 -2
  55. package/dist/cores/elastic/index.js +3 -2
  56. package/dist/cores/elastic/index.js.map +1 -1
  57. package/dist/cores/elastic/os2-query-service.d.ts +104 -0
  58. package/dist/cores/elastic/os2-query-service.js +495 -0
  59. package/dist/cores/elastic/os2-query-service.js.map +1 -0
  60. package/dist/cores/elastic/os2-service.d.ts +372 -0
  61. package/dist/cores/elastic/os2-service.js +943 -0
  62. package/dist/cores/elastic/os2-service.js.map +1 -0
  63. package/dist/cores/index.d.ts +6 -1
  64. package/dist/cores/index.js +9 -2
  65. package/dist/cores/index.js.map +1 -1
  66. package/dist/cores/lambda/index.js +2 -1
  67. package/dist/cores/lambda/index.js.map +1 -1
  68. package/dist/cores/lambda/lambda-cron-handler.d.ts +1 -0
  69. package/dist/cores/lambda/lambda-cron-handler.js +5 -1
  70. package/dist/cores/lambda/lambda-cron-handler.js.map +1 -1
  71. package/dist/cores/lambda/lambda-dynamo-stream-handler.d.ts +5 -4
  72. package/dist/cores/lambda/lambda-dynamo-stream-handler.js +18 -5
  73. package/dist/cores/lambda/lambda-dynamo-stream-handler.js.map +1 -1
  74. package/dist/cores/lambda/lambda-handler.d.ts +22 -2
  75. package/dist/cores/lambda/lambda-handler.js +85 -23
  76. package/dist/cores/lambda/lambda-handler.js.map +1 -1
  77. package/dist/cores/lambda/lambda-sns-handler.d.ts +1 -0
  78. package/dist/cores/lambda/lambda-sns-handler.js +5 -0
  79. package/dist/cores/lambda/lambda-sns-handler.js.map +1 -1
  80. package/dist/cores/lambda/lambda-sqs-handler.d.ts +1 -0
  81. package/dist/cores/lambda/lambda-sqs-handler.js +20 -5
  82. package/dist/cores/lambda/lambda-sqs-handler.js.map +1 -1
  83. package/dist/cores/lambda/lambda-web-handler.d.ts +10 -0
  84. package/dist/cores/lambda/lambda-web-handler.js +107 -41
  85. package/dist/cores/lambda/lambda-web-handler.js.map +1 -1
  86. package/dist/cores/protocol/aws-lambda-helper.js +58 -11
  87. package/dist/cores/protocol/aws-lambda-helper.js.map +1 -1
  88. package/dist/cores/protocol/index.js +3 -0
  89. package/dist/cores/protocol/index.js.map +1 -1
  90. package/dist/cores/protocol/protocol-service.d.ts +26 -5
  91. package/dist/cores/protocol/protocol-service.js +42 -12
  92. package/dist/cores/protocol/protocol-service.js.map +1 -1
  93. package/dist/cores/protocol/protocol-sign.d.ts +72 -0
  94. package/dist/cores/protocol/protocol-sign.js +194 -0
  95. package/dist/cores/protocol/protocol-sign.js.map +1 -0
  96. package/dist/cores/storage/index.d.ts +0 -1
  97. package/dist/cores/storage/index.js +0 -1
  98. package/dist/cores/storage/index.js.map +1 -1
  99. package/dist/cores/storage/model-fields.d.ts +7 -0
  100. package/dist/cores/storage/model-fields.js +21 -0
  101. package/dist/cores/storage/model-fields.js.map +1 -0
  102. package/dist/cores/storage/model-manager.d.ts +3 -2
  103. package/dist/cores/storage/model-manager.js +7 -3
  104. package/dist/cores/storage/model-manager.js.map +1 -1
  105. package/dist/engine/builder.js +20 -2
  106. package/dist/engine/builder.js.map +1 -1
  107. package/dist/engine/engine.d.ts +2 -2
  108. package/dist/engine/engine.js +4 -4
  109. package/dist/engine/engine.js.map +1 -1
  110. package/dist/engine/utilities.d.ts +11 -12
  111. package/dist/engine/utilities.js +30 -82
  112. package/dist/engine/utilities.js.map +1 -1
  113. package/dist/environ.d.ts +0 -6
  114. package/dist/environ.js +1 -12
  115. package/dist/environ.js.map +1 -1
  116. package/dist/esm/index.mjs +140 -0
  117. package/dist/esm/testing.mjs +12 -0
  118. package/dist/extended/abstract-service.d.ts +48 -139
  119. package/dist/extended/abstract-service.js +46 -382
  120. package/dist/extended/abstract-service.js.map +1 -1
  121. package/dist/extended/cores/abstract-controllers.d.ts +170 -0
  122. package/dist/extended/cores/abstract-controllers.js +533 -0
  123. package/dist/extended/cores/abstract-controllers.js.map +1 -0
  124. package/dist/extended/cores/abstract-rest-apis.d.ts +142 -0
  125. package/dist/extended/cores/abstract-rest-apis.js +203 -0
  126. package/dist/extended/cores/abstract-rest-apis.js.map +1 -0
  127. package/dist/extended/cores/abstract-services.d.ts +771 -0
  128. package/dist/extended/cores/abstract-services.js +2042 -0
  129. package/dist/extended/cores/abstract-services.js.map +1 -0
  130. package/dist/extended/cores/browser-cache.d.ts +2 -0
  131. package/dist/extended/cores/browser-cache.js +34 -0
  132. package/dist/extended/cores/browser-cache.js.map +1 -0
  133. package/dist/extended/cores/commons.d.ts +20 -0
  134. package/dist/extended/cores/commons.js +31 -0
  135. package/dist/extended/cores/commons.js.map +1 -0
  136. package/dist/extended/cores/index.d.ts +15 -0
  137. package/dist/extended/cores/index.js +32 -0
  138. package/dist/extended/cores/index.js.map +1 -0
  139. package/dist/extended/cores/types.d.ts +14 -0
  140. package/dist/extended/cores/types.js +31 -0
  141. package/dist/extended/cores/types.js.map +1 -0
  142. package/dist/helpers/helpers.d.ts +10 -5
  143. package/dist/helpers/helpers.js +46 -58
  144. package/dist/helpers/helpers.js.map +1 -1
  145. package/dist/helpers/types.d.ts +22 -10
  146. package/dist/index.d.ts +4 -0
  147. package/dist/index.js +12 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/lib/http-fetch.d.ts +54 -0
  150. package/dist/lib/http-fetch.js +133 -0
  151. package/dist/lib/http-fetch.js.map +1 -0
  152. package/dist/lib/lazy.d.ts +22 -0
  153. package/dist/lib/lazy.js +54 -0
  154. package/dist/lib/lazy.js.map +1 -0
  155. package/dist/lib/perf.d.ts +35 -0
  156. package/dist/lib/perf.js +52 -0
  157. package/dist/lib/perf.js.map +1 -0
  158. package/dist/tools/tools.d.ts +15 -3
  159. package/dist/tools/tools.js +21 -4
  160. package/dist/tools/tools.js.map +1 -1
  161. package/package.json +96 -39
  162. package/dist/cores/storage/redis-storage-service.d.ts +0 -183
  163. package/dist/cores/storage/redis-storage-service.js +0 -377
  164. package/dist/cores/storage/redis-storage-service.js.map +0 -1
  165. package/dist/generated/field-registry.d.ts +0 -10
  166. package/dist/generated/field-registry.js +0 -17
  167. package/dist/generated/field-registry.js.map +0 -1
@@ -0,0 +1,2042 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.$ES6Routed = exports.$OS2 = exports._OS2 = exports.OS2Instance = exports.OS2FGACManager = exports.OS2Service = exports.MyManagerProxy = exports.MyCoreProxy = exports.MyCoreManager = exports.MyCoreService = exports.sourceToItem = void 0;
37
+ /**
38
+ * `abstract-services.ts`
39
+ * - common services used in `/cores`
40
+ *
41
+ * @author Steve <steve@lemoncloud.io>
42
+ * @date 2022-06-15 initial version
43
+ * @date 2022-06-24 fixed some failures.
44
+ * @date 2022-07-27 support { initialNo, throwable }
45
+ * @date 2022-08-04 opt `list()` w/ sort by createdAt:desc
46
+ * @date 2022-08-09 use `SessionToken` from context
47
+ * @date 2022-11-04 filter of `not exists` if param==''
48
+ * @date 2022-11-15 use `buildQuery()` to build search-body.
49
+ * @date 2022-12-29 optimized with `lemon-core@3.2.1`
50
+ * @date 2023-01-13 optimized `makeModel()`
51
+ * @date 2023-01-18 optimized with `lemon-core@3.2.4`
52
+ * @date 2023-01-20 optimized `doSearch()` w/ `SearchParam()`
53
+ * @date 2023-01-31 fixed null error of `model[key]`
54
+ * @date 2023-02-15 optimized with `lemon-core@3.2.5`
55
+ * @date 2023-09-15 optimized `getCurrentSession()` w/ USE_SESSION + normalize option.
56
+ * @date 2023-10-08 optimized `buildQuery()` w/ sort + offset.
57
+ * @date 2023-10-11 optimized `doScan()` w/ dynamo-scan-filter.
58
+ * @date 2024-11-01 optimized `nextId()` of manager-proxy
59
+ * @date 2023-11-06 optimized `onAfterSave()` for post-processing.
60
+ * @date 2024-11-14 renamed to `IdentityToken` from `SessionToken`
61
+ * @date 2024-11-28 optimized `assertContext()` and `isAdminRole()` w/ `lemon-core@3.2.11`.
62
+ * @date 2025-01-03 optimized `buildQuery()` w/ sort + offset.
63
+ * @date 2025-03-24 optimized `OS2Instance` with `lemon-core@3.2.13`
64
+ * @date 2025-07-17 optimized `onAfterSave` with `lemon-core@4.0.4`
65
+ * @date 2025-07-23 optimized `buildQuery()` with `hidden` + `aggregation` option.
66
+ * @date 2025-08-01 optimized `canDo()` for better security w/ session.
67
+ * @date 2025-08-11 optimized `OS2FGACManager` for latest features.
68
+ * @date 2025-08-14 optimized `OS2.deleteIndex()` for latest features.
69
+ * @date 2025-09-05 optimized `OS2FGACManager` environment variables.
70
+ * @date 2025-10-31 optimized `session` with `lemon-devkit@0.0.8`.
71
+ * @date 2025-12-01 optimized `makeModel()` w/ T.
72
+ * @date 2025-12-15 optimized `getCurrentSession()` with local meta info.
73
+ * @date 2026-01-05 optimized `hasAdminRole()` and `getCurrentSession()`.
74
+ * @date 2026-02-11 optimized `proxy.mget()` with `throwable` options.
75
+ * @date 2026-04-09 optimized `types` to fix `ModelType` cast error.
76
+ * @date 2026-10-27 v5 B2-b: OpenSearch client, lazy `$OS2` (= `$ES6Routed`), one service set per instance.
77
+ * @date 2026-10-28 v5 B4: `CORE_FIELDS = fieldsOf(CoreModel)`; manager `fields` may be a model class.
78
+ *
79
+ * @copyright (C) 2022 LemonCloud Co Ltd. - All Rights Reserved.
80
+ * @origin `@lemoncloud/lemon-templates-api/cores`
81
+ */
82
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
83
+ const engine_1 = require("../../engine/");
84
+ const cores_1 = __importStar(require("../../cores/"));
85
+ const cores_2 = require("../../cores/");
86
+ const test_helper_1 = require("../../common/test-helper");
87
+ const tools_1 = require("../../tools/");
88
+ const helpers_1 = require("../../helpers/");
89
+ const abstract_service_1 = require("../abstract-service");
90
+ //* v5 B3: the local copy was identical to the L1 one (the root already used that); re-exported to keep this deep path.
91
+ var abstract_service_2 = require("../abstract-service");
92
+ Object.defineProperty(exports, "sourceToItem", { enumerable: true, get: function () { return abstract_service_2.sourceToItem; } });
93
+ const lazy_1 = require("../../lib/lazy");
94
+ const perf_1 = require("../../lib/perf");
95
+ const lemon_model_1 = require("lemon-model");
96
+ const commons_1 = require("./commons");
97
+ const NAME = engine_1.$U.env('NAME', 'lemon');
98
+ const NS = engine_1.$U.NS('HDBS', 'green');
99
+ /**
100
+ * fields in core model (L2: the 14 L1 `CORE_FIELDS` + `id`, `_id`).
101
+ * - v5 B4: read from the `@field()` annotations of the `CoreModel` class (was the generated registry).
102
+ */
103
+ const CORE_FIELDS = (0, lemon_model_1.fieldsOf)(lemon_model_1.CoreModel);
104
+ /**
105
+ * lookup table for common types.
106
+ */
107
+ const $LUT = {
108
+ /**
109
+ * type: `CanDoAction`
110
+ */
111
+ CanDoAction: {
112
+ list: 'list',
113
+ read: 'read',
114
+ create: 'create',
115
+ update: 'update',
116
+ delete: 'delete',
117
+ },
118
+ };
119
+ /**
120
+ * class: `MyCoreService`
121
+ * - CoreService의 공통 부분을 채워넣는다.
122
+ */
123
+ class MyCoreService extends abstract_service_1.CoreService {
124
+ constructor(tableName, ns) {
125
+ super(tableName, ns);
126
+ }
127
+ /**
128
+ * make proxy, and use callback to handle.
129
+ * - it will save automatically on success.
130
+ * - v5: on exception it saves only when `options.saveOnError` is true (default false, B5 § 6.7).
131
+ */
132
+ guardProxy = async (context, callback, options) => {
133
+ const proxy = this.createProxy(context);
134
+ try {
135
+ const result = await callback(proxy);
136
+ const saved = await proxy.saveAllUpdates();
137
+ (0, engine_1._log)(`> saved[${saved?.length}] =`, engine_1.$U.json(saved));
138
+ return result;
139
+ }
140
+ catch (e) {
141
+ //* v5: save pending updates on exception only by opt-in (v4 always saved @240613).
142
+ if (options?.saveOnError) {
143
+ const saved = await proxy.saveAllUpdates().catch(e => (0, test_helper_1.GETERR$)(e));
144
+ (0, engine_1._log)(`> saved =`, engine_1.$U.json(saved));
145
+ }
146
+ throw e;
147
+ }
148
+ };
149
+ /**
150
+ * run search (via `$OS2`)
151
+ * - override this to hide `$OS2`
152
+ */
153
+ async doSearch(body) {
154
+ (0, engine_1._log)(`doSearch()`);
155
+ (0, engine_1._log)(`> list.query =`, engine_1.$U.json(body));
156
+ const result = await exports.$OS2.search(body);
157
+ (0, engine_1._log)(`> list.res =`, engine_1.$U.json({ ...result, list: [result?.list?.[0]] }));
158
+ return result;
159
+ }
160
+ /**
161
+ * check if using useSession from environ.
162
+ */
163
+ isUseSession() {
164
+ const USE_SESSION = commons_1.$T.S(engine_1.$U.env('USE_SESSION', '')).toLowerCase();
165
+ return USE_SESSION && USE_SESSION != '0' && USE_SESSION != 'false' ? true : false;
166
+ }
167
+ /**
168
+ * check if search-engine is supportable
169
+ * @returns
170
+ */
171
+ hasSearchEngine() {
172
+ const useSearch = !!exports.$OS2.options?.indexName;
173
+ return useSearch;
174
+ }
175
+ }
176
+ exports.MyCoreService = MyCoreService;
177
+ /**
178
+ * class: `CoreManager`
179
+ * - shared core manager for all model
180
+ *
181
+ * @abstract
182
+ */
183
+ class MyCoreManager extends abstract_service_1.CoreManager {
184
+ options;
185
+ /**
186
+ * namespace for log(print)
187
+ */
188
+ logNS;
189
+ /**
190
+ * constructor
191
+ * @protected
192
+ */
193
+ constructor(type, parent,
194
+ //* v5 B4: a `string[]` or a model class (resolved in `AbstractManager`).
195
+ fields, options) {
196
+ super(type, parent, fields, options?.uniqueField);
197
+ this.options = options;
198
+ //* v5: sync is opt-in (default false), so `$OS2` is built only when a manager asks (B5 § 6.7).
199
+ const enableSync = options?.enableSync ?? false;
200
+ //* the first manager with `enableSync: true` builds the lazy `$OS2` (1 listener when `OS2_ENDPOINT` is set).
201
+ if (enableSync)
202
+ exports.$OS2.synchronizer?.enableSynchronization(this.type);
203
+ this.logNS = engine_1.$U.NS(type, 'yellow');
204
+ }
205
+ /**
206
+ * get the next sequenced no from `initialNo` (default 1000000)
207
+ *
208
+ * @param step (optional) next-step (>= 0)
209
+ * @returns next seq-no
210
+ */
211
+ async nextId(step) {
212
+ return this.storage.storage.nextSeq(this.storage.type, this.options?.initialNo, step);
213
+ }
214
+ /**
215
+ * build default model for creation.
216
+ */
217
+ buildDefault(id) {
218
+ //WARN! do not include { last, balance } (avoid conflict)
219
+ const $def = this.prepareDefault({ id });
220
+ const $key = this.parent.asKey$(this.type, id);
221
+ return (0, commons_1.onlyDefined)({ ...$def, ...$key, _id: undefined });
222
+ }
223
+ /**
224
+ * validate the request's model before saving.
225
+ * - `$org`에 따라서, 신규 생성인지 업데이트인지 구분 가능함.
226
+ *
227
+ * @param model request's model
228
+ * @param $org (optional) the origin model (valid ONLY if 업데이트)
229
+ * @returns validatated model.
230
+ */
231
+ async validateModel(model, $org) {
232
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
233
+ const isCreate = !$org; // flag to indicate new creation.
234
+ return model;
235
+ }
236
+ /**
237
+ * find lookup-model (improved @2022)
238
+ * - lookup to `auto-id` by key.
239
+ * - if not exits, make the new id (auto-seq) and store the key.
240
+ * - type should be like `#<type>`.
241
+ *
242
+ * NOTE! must to support race condition.
243
+ *
244
+ * @param key any string
245
+ */
246
+ async findLookup$(key, params) {
247
+ //! valida parameters...
248
+ const resetRate = params?.resetRate ?? 0.9; // upper rate to reset lock
249
+ const useSha256 = params?.useSha256 ?? true; // flag to use sha256 algorithm.
250
+ const isUseMeta = params?.isUseMeta ?? false; // flag to use meta
251
+ //! build hash...
252
+ const text = typeof key === 'number' ? `${key}` : `${key || ''}`;
253
+ const hash = useSha256 ? engine_1.$U.md5(text, 'base64') : engine_1.$U.hash(text);
254
+ const keyId = `#${hash}.${text.length}`;
255
+ //! recursive func to repeat
256
+ const asyncAuto = async ($base, depth = 0) => {
257
+ const keyId = $base.id;
258
+ //NOTE - it will throw if no lookup exists.
259
+ const $org = await this.storage.read(keyId).catch(e => {
260
+ if (params?.throwable)
261
+ throw e;
262
+ return (0, test_helper_1.NUL404)(e);
263
+ });
264
+ //! try to initialize the lookup key.......
265
+ if (!$org || !$org?.next) {
266
+ const $inc = { lock: 1 };
267
+ const $ups = { stereo: $base.stereo, meta: $base.meta };
268
+ const $lock = await this.storage.increment(keyId, $inc, $ups);
269
+ if ($lock.lock === 1) {
270
+ // const nextId = await this.storage.storage.nextSeq(this.storage.type, params?.initialNo);
271
+ const nextId = await this.nextId();
272
+ const $ups = {
273
+ id: keyId,
274
+ type: `#${this.type}`,
275
+ next: nextId,
276
+ createdAt: $lock.createdAt ?? $lock.updatedAt,
277
+ deletedAt: 0,
278
+ };
279
+ const $updated = await this.storage.update(keyId, $ups);
280
+ return { ...$lock, ...$updated };
281
+ }
282
+ else if (depth > 100) {
283
+ throw new Error(`@depth[${depth}] is out of range - findLookup(${$base?.meta?.text})`);
284
+ }
285
+ //! in some rate, try to reset lock.
286
+ const rate = Math.random();
287
+ const wait = Math.round(rate * 10);
288
+ return (0, test_helper_1.waited)(wait)
289
+ .then(() => (rate > resetRate ? this.storage.update(keyId, { lock: 0 }) : null))
290
+ .then(() => asyncAuto($base, depth + 1));
291
+ }
292
+ return $org;
293
+ };
294
+ //! make base model, and start loop.
295
+ const meta = isUseMeta ? { hash, text } : undefined;
296
+ const stereo = isUseMeta ? '#' : `#${text}`;
297
+ return asyncAuto({ id: keyId, meta, stereo });
298
+ }
299
+ /**
300
+ * build search query from filters
301
+ */
302
+ buildQuery(param, model, options) {
303
+ const errScope = options?.errScope ?? `buildQuery(${this.type})`;
304
+ if (typeof param?.limit !== 'number')
305
+ throw new Error(`.limit(number) is required - ${errScope}`);
306
+ if (param?.page && typeof param?.page !== 'number')
307
+ throw new Error(`.page(number) is required - ${errScope}`);
308
+ if (param?.offset && typeof param?.offset !== 'number')
309
+ throw new Error(`.offset(number) is required - ${errScope}`);
310
+ const aggregation = options?.aggregation ?? 'stereo'; // default aggregation
311
+ const aggrLimit = options?.aggrLimit ?? 12; // default aggregation limit
312
+ // STEP.0 validate parameters.
313
+ const { limit } = param;
314
+ const page = commons_1.$T.N(param?.page, 0);
315
+ const offset = commons_1.$T.N(param?.offset, 0);
316
+ // 1. build search request body
317
+ const body = {
318
+ size: limit,
319
+ from: limit * page + offset,
320
+ query: {
321
+ bool: {
322
+ filter: [
323
+ { term: { 'ns.keyword': this.parent.NS } },
324
+ { term: { 'type.keyword': this.type } },
325
+ { term: { deletedAt: 0 } },
326
+ ],
327
+ },
328
+ },
329
+ };
330
+ //* apply aggregation if needed.
331
+ if (aggregation && typeof aggregation === 'string' && aggregation.length > 0) {
332
+ body.aggs = {
333
+ [aggregation]: {
334
+ terms: {
335
+ field: `${aggregation}.keyword`,
336
+ missing: 'N/A',
337
+ order: { _key: 'asc' },
338
+ size: aggrLimit,
339
+ },
340
+ },
341
+ };
342
+ }
343
+ // 1-0. default filter.
344
+ if (model?.stereo === null) {
345
+ // _add_must_not({ term: { [`stereo.keyword`]: '#' } });
346
+ this.addQueryTerm(body, 'must_not', { term: { [`stereo.keyword`]: '#' } });
347
+ }
348
+ // 1-1. support `hidden` flag.
349
+ if (options?.hidden !== undefined) {
350
+ const hidden = Boolean(options?.hidden);
351
+ if (hidden) {
352
+ this.addQueryTerm(body, 'must', { term: { hidden: 1 } });
353
+ }
354
+ else {
355
+ this.addQueryTerm(body, 'must_not', { term: { hidden: 1 } });
356
+ }
357
+ }
358
+ // 1-2. filter by attributes.
359
+ if (model && Object.keys(model).length > 0) {
360
+ (0, engine_1._inf)(this.logNS, `> filters =`, engine_1.$U.json(model));
361
+ Object.entries(model).forEach(([key, val]) => {
362
+ if (typeof val === 'string') {
363
+ if (val === '') {
364
+ this.addQueryTerm(body, 'must_not', { exists: { field: `${key}` } });
365
+ }
366
+ else if (val === '!') {
367
+ this.addQueryTerm(body, 'must', { exists: { field: `${key}` } });
368
+ }
369
+ else {
370
+ this.addQueryTerm(body, 'filter', { term: { [`${key}.keyword`]: val } });
371
+ }
372
+ }
373
+ else if (val === null) {
374
+ // ignore
375
+ }
376
+ else if (typeof val === 'number') {
377
+ this.addQueryTerm(body, 'filter', { term: { [`${key}`]: val } });
378
+ }
379
+ else if (typeof val === 'object' && Array.isArray(val)) {
380
+ this.addQueryTerm(body, 'must', {
381
+ bool: {
382
+ should: val.map(n => ({
383
+ term: {
384
+ [`${key}.keyword`]: n,
385
+ },
386
+ })),
387
+ minimum_should_match: 1,
388
+ },
389
+ });
390
+ }
391
+ });
392
+ }
393
+ // add sort term by `param.sort`
394
+ return this.addSortTerm(body, param);
395
+ }
396
+ /**
397
+ * Query 기준 추가하기.
398
+ */
399
+ addQueryTerm(body, term, query) {
400
+ if (!body)
401
+ throw new Error(`@body (SearchBody) is required!`);
402
+ if (!body.query || !body.query.bool) {
403
+ body.query = { bool: { filter: [], must: [], must_not: [] } };
404
+ }
405
+ //* helper function.
406
+ const _add_filter = (query) => {
407
+ const filter = [...(body.query.bool.filter || []), ...(Array.isArray(query) ? query : [query])];
408
+ body.query.bool.filter = filter;
409
+ };
410
+ const _add_must = (query) => {
411
+ const must = [...(body.query.bool.must || []), ...(Array.isArray(query) ? query : [query])];
412
+ body.query.bool.must = must;
413
+ };
414
+ const _add_must_not = (query) => {
415
+ const must_not = [
416
+ ...(body.query.bool.must_not || []),
417
+ ...(Array.isArray(query) ? query : [query]),
418
+ ];
419
+ body.query.bool.must_not = must_not;
420
+ };
421
+ if (term === 'filter') {
422
+ _add_filter(query);
423
+ }
424
+ else if (term === 'must') {
425
+ _add_must(query);
426
+ }
427
+ else if (term === 'must_not') {
428
+ _add_must_not(query);
429
+ }
430
+ else {
431
+ throw new Error(`@term(${term}) is not supported!`);
432
+ }
433
+ return body;
434
+ }
435
+ /**
436
+ * sort 기준 추가하기.
437
+ */
438
+ addSortTerm(body, param) {
439
+ if (!body.sort)
440
+ body.sort = { createdAt: { order: 'desc', missing: '_last' } };
441
+ if (param?.sort) {
442
+ const [sort, asc] = commons_1.$T.S(param?.sort).split(':', 2);
443
+ const order = `${asc || 'asc'}`;
444
+ const missing = order == 'desc' ? '_last' : '_first';
445
+ if (!sort)
446
+ body.sort = undefined;
447
+ else if (sort === 'id')
448
+ body.sort = { 'id.keyword': { order, missing: '_last' } };
449
+ else if (sort === 'name')
450
+ body.sort = { 'name.keyword': { order, missing: '_last' } };
451
+ else if (sort === 'brand')
452
+ body.sort = { 'brand.keyword': { order, missing: '_last' } };
453
+ else if (sort === 'color')
454
+ body.sort = { 'color.keyword': { order, missing: '_last' } };
455
+ else if (sort === 'order')
456
+ body.sort = { order: { order, missing } };
457
+ else if (sort === 'createdAt')
458
+ body.sort = { createdAt: { order, missing } };
459
+ else if (sort === 'updatedAt')
460
+ body.sort = { updatedAt: { order, missing } };
461
+ else if (sort)
462
+ body.sort = { [`${sort}.keyword`]: { order, missing } };
463
+ }
464
+ return body;
465
+ }
466
+ /**
467
+ * build filter for scan.
468
+ * @returns DynamoScanFilter
469
+ */
470
+ buildScanFilter(params) {
471
+ const filter = [
472
+ { key: 'ns', comparator: '=', value: this.parent.NS },
473
+ { key: 'type', comparator: '=', value: this.type },
474
+ { not: { key: 'stereo', operator: 'begins_with', value: '#' } },
475
+ { key: 'deletedAt', comparator: '=', value: 0 },
476
+ ];
477
+ // filter by params
478
+ if (params) {
479
+ Object.entries(params).forEach(([key, val]) => {
480
+ if (val === undefined || val === null) {
481
+ // ignore.
482
+ }
483
+ else if (typeof val === 'string') {
484
+ filter.push({ key, comparator: '=', value: val });
485
+ }
486
+ else {
487
+ throw new Error(`400 NOT SUPPORTED - filter param[${engine_1.$U.json(params)}]`);
488
+ }
489
+ });
490
+ }
491
+ return filter;
492
+ }
493
+ /**
494
+ * execute scan with filter.
495
+ *
496
+ * @param $page
497
+ * @param filter
498
+ * @returns
499
+ */
500
+ async doScan($page, filter) {
501
+ const options = this.parent.dynamoOptions;
502
+ const idName = options.idName || '_id';
503
+ //* check if dummy table
504
+ if (!options?.tableName || options?.tableName?.endsWith('.yml'))
505
+ throw new Error(`400 NO DYNAMO - table:${options?.tableName ?? ''}!`);
506
+ const $scan = new cores_2.DynamoScanService(options);
507
+ //* scan all items with basic filter.
508
+ const list0 = [];
509
+ let $res;
510
+ while (true) {
511
+ $res = await $scan.scan(-1, $res?.last, filter);
512
+ $res.list?.forEach(N => {
513
+ delete N[idName];
514
+ list0.push(N);
515
+ });
516
+ if (!$res?.last[idName])
517
+ break;
518
+ }
519
+ //TODO - sort by some.
520
+ //* check if in unlimited. (or cut-off by page)
521
+ const limit = $page?.limit ?? 10;
522
+ const page = $page?.page ?? 0;
523
+ const unLimited = limit === -1 ? true : false;
524
+ const total = list0?.length || 0;
525
+ const _page = (list, page, limit) => {
526
+ if (!unLimited && limit === 0)
527
+ return [];
528
+ else if (unLimited)
529
+ return list;
530
+ return list.slice(page * limit, (page + 1) * limit);
531
+ };
532
+ const list = _page(list0, page, limit);
533
+ //* returns.
534
+ const aggr = [{ key: '#scan', count: total }];
535
+ return { list, total, limit, aggr };
536
+ }
537
+ /**
538
+ * execute scan-last with filter.
539
+ *
540
+ * @param $param ScanLastParams
541
+ * @returns
542
+ */
543
+ async doScanLast($param) {
544
+ const options = this.parent.dynamoOptions;
545
+ const limit = $param?.limit ?? 2;
546
+ const last = $param?.last;
547
+ const filter = $param?.filter;
548
+ if (!options?.tableName || options?.tableName?.endsWith('.yml')) {
549
+ throw new Error(`400 NO DYNAMO - table:${options?.tableName ?? ''}!`);
550
+ }
551
+ const $scan = new cores_2.DynamoScanService(options);
552
+ const $res = await $scan.scan(limit, last, filter);
553
+ const list = $res.list;
554
+ const total = engine_1.$U.N(list?.length || 0);
555
+ return (0, commons_1.onlyDefined)({
556
+ list,
557
+ total,
558
+ limit: limit,
559
+ count: $res?.count,
560
+ last: Object.keys($res?.last || {}).length === 0 ? undefined : $res?.last,
561
+ });
562
+ }
563
+ /**
564
+ * basic ElasticSearch Query.
565
+ * you can override this method for customization.
566
+ *
567
+ * @param limit pagination limit (default 10)
568
+ * @param page page number (starts from 0)
569
+ * @param filters model filters
570
+ * @param param (optional) addtion params
571
+ */
572
+ async list($page, $param, options) {
573
+ (0, engine_1._log)(this.logNS, `list(${$page?.limit ?? ''}, ${$page?.page ?? ''})..`);
574
+ const aggregation = options?.aggregation ?? 'stereo'; // default aggregation
575
+ //* use dynamo-scan if no search configuration.
576
+ if (!this.parent.hasSearchEngine()) {
577
+ const filter = this.buildScanFilter($param);
578
+ return this.doScan($page, filter);
579
+ }
580
+ // 1. build search request body
581
+ const body = this.buildQuery($page, $param, options);
582
+ // 2. search w/ query
583
+ const result = await this.parent.doSearch(body);
584
+ // 3. extract names only
585
+ const _aggr = () => {
586
+ if (!aggregation)
587
+ return [];
588
+ const buckets = result?.aggregations?.[aggregation]?.buckets;
589
+ buckets && (0, engine_1._log)(this.logNS, `> buckets =`, engine_1.$U.json(buckets));
590
+ if (!buckets || !Array.isArray(buckets))
591
+ return [];
592
+ return buckets?.reduce((L, bucket) => {
593
+ const key = commons_1.$T.S(bucket.key);
594
+ const count = commons_1.$T.N(bucket.doc_count, 0);
595
+ L.push({ key, count });
596
+ return L;
597
+ }, []);
598
+ };
599
+ // returns.
600
+ return { total: result.total, list: result.list, aggr: _aggr(), limit: $page?.limit, page: $page?.page };
601
+ }
602
+ /**
603
+ * list of projection fields
604
+ * - ONLY fields defined in `goods-model`
605
+ * - 모델별 추가 필드목록만 추림
606
+ */
607
+ listProjections(params) {
608
+ const includes = params?.includes ?? ['id', 'error', 'createdAt'];
609
+ const excludes = params?.excludes ?? ['extra', 'desc'];
610
+ const isSorted = params?.sorted ?? true;
611
+ const _notIn = (list, $ref) => list.filter(s => !excludes?.includes(s) && !includes?.includes(s) && !$ref?.includes(s));
612
+ const list = _notIn(this.FIELDS, CORE_FIELDS);
613
+ if (isSorted)
614
+ return [...includes, ...list.sort()];
615
+ return [...includes, ...list];
616
+ }
617
+ /**
618
+ * fetch all list of goods for filtering.
619
+ *
620
+ * @override if required.
621
+ */
622
+ buildSearchAll(params, sort) {
623
+ sort = sort ?? { createdAt: { order: 'asc', missing: '_last' } };
624
+ const [ns, type] = [this.parent.NS, this.type];
625
+ const size = params?.size ?? 500;
626
+ const _source = this.listProjections({ excludes: ['extra', 'meta'] });
627
+ const body = {
628
+ size,
629
+ query: {
630
+ bool: {
631
+ filter: [{ term: { 'ns.keyword': ns } }, { term: { 'type.keyword': type } }],
632
+ },
633
+ },
634
+ _source,
635
+ };
636
+ if (sort)
637
+ body.sort = sort;
638
+ const _add_filter = (query) => {
639
+ const filter = [...(body.query.bool.filter || []), ...(Array.isArray(query) ? query : [query])];
640
+ body.query.bool.filter = filter;
641
+ };
642
+ const _add_must = (query) => {
643
+ const must = [...(body.query.bool.must || []), ...(Array.isArray(query) ? query : [query])];
644
+ body.query.bool.must = must;
645
+ };
646
+ const _add_must_not = (query) => {
647
+ const must_not = [
648
+ ...(body.query.bool.must_not || []),
649
+ ...(Array.isArray(query) ? query : [query]),
650
+ ];
651
+ body.query.bool.must_not = must_not;
652
+ };
653
+ _add_must({
654
+ bool: {
655
+ should: [
656
+ { range: { deletedAt: { lte: 0 } } },
657
+ {
658
+ bool: {
659
+ must_not: {
660
+ exists: {
661
+ field: 'deletedAt',
662
+ },
663
+ },
664
+ },
665
+ },
666
+ ],
667
+ minimum_should_match: 1,
668
+ },
669
+ });
670
+ _add_must_not({ term: { stereo: '#' } });
671
+ _add_must_not({ term: { 'stereo.keyword': '#' } });
672
+ return body;
673
+ }
674
+ /**
675
+ * sync by scan with last record.
676
+ *
677
+ * **[좋은코드]**
678
+ * 1. 보기 좋아야함; 변수이름, 로직도 잘보이고, 함수의 목적
679
+ * 2. 테스트 가능한 코드 (또는 테스트가된 코드)
680
+ * - Unit Test: 테스트 가능한 최소단위 (함수.. 블럭)
681
+ * - Integrated Test: Group of Units (ex: my_sequence)
682
+ *
683
+ * @param $param
684
+ * @param options
685
+ * @returns
686
+ */
687
+ async doSyncScanLast($param, options) {
688
+ //* 1. doScanLast(w/filter by type); scan-size = 100
689
+ const $perf = commons_1.$T.perf();
690
+ const { list, last, count } = await this.doScanLast($param);
691
+ //* 2. chunk the list into smaller parts
692
+ const _makeChucks = (list, chunkSize) => {
693
+ const result = [];
694
+ for (let i = 0; i < list.length; i += chunkSize) {
695
+ result.push(list.slice(i, i + chunkSize));
696
+ }
697
+ return result;
698
+ };
699
+ const _saveBulk = async (operations) => {
700
+ const elastic = exports.$OS2.client;
701
+ const $res = await elastic.bulk({ body: operations }).catch(e => {
702
+ (0, engine_1._err)(this.NS, `>> err=`, e);
703
+ throw e;
704
+ });
705
+ //* the OS client types `items[].index.error` as an object; read it in the v4 shape.
706
+ const list = $res.body?.items?.map(item => ({
707
+ _id: item.index._id,
708
+ _version: item.index._version,
709
+ error: item.index.error,
710
+ status: item.index.status,
711
+ })) ?? [];
712
+ return { list, error: null };
713
+ };
714
+ const chunkSize = options?.size ?? 10;
715
+ const chunks = _makeChucks(list, chunkSize);
716
+ //* 3. use my_sequence to bulk each chunk sequentially
717
+ const chunkList = chunks.map((chunk, index) => ({
718
+ id: `${index}`,
719
+ error: null,
720
+ chunk,
721
+ }));
722
+ const _transformBulkOperations = (dataChunk) => {
723
+ return dataChunk.flatMap(doc => {
724
+ const { _id, ...items } = doc;
725
+ return [{ index: { _index: exports.$OS2.options?.indexName, _id: doc._id } }, items];
726
+ });
727
+ };
728
+ const results = await (0, helpers_1.my_sequence)(chunkList, async (N) => {
729
+ const operations = _transformBulkOperations(N.chunk);
730
+ const bulk = await _saveBulk(operations).catch(e => ({ list: [], error: (0, test_helper_1.GETERR)(e) }));
731
+ return { ...N, ...bulk };
732
+ });
733
+ // _log(this.logNS, `> results =`, $U.json(results));
734
+ // validate input and output _id
735
+ const _validateIds = (inputList, outputList) => {
736
+ const mismatchedIds = outputList.filter(item => !inputList.some(input => input._id === item._id));
737
+ if (mismatchedIds.length > 0) {
738
+ const mismatchedIdsList = mismatchedIds.map(item => item._id);
739
+ return { isValid: false, mismatchedIds: mismatchedIdsList };
740
+ }
741
+ return { isValid: true, mismatchedIds: [] };
742
+ };
743
+ //TODO - 초기 입력의 목록과 출력의 목록의 쌍이 서로 맞아야함.... @241230/steve
744
+ const itemList = results.reduce((L, N) => {
745
+ if (typeof N?.error == 'string')
746
+ L.push({ error: N.error });
747
+ if (N?.list) {
748
+ const validation = _validateIds(list, N?.list);
749
+ if (!validation.isValid)
750
+ throw new Error(`Mismatched list id found: ${validation.mismatchedIds}`);
751
+ L.push(...N.list);
752
+ }
753
+ return L;
754
+ }, []);
755
+ //* returns
756
+ const took = $perf.took();
757
+ return (0, commons_1.onlyDefined)({ last: last, count: count, took, list: itemList });
758
+ }
759
+ }
760
+ exports.MyCoreManager = MyCoreManager;
761
+ /**
762
+ * class: `MyCoreProxy`
763
+ * - common parent of proxy.
764
+ */
765
+ class MyCoreProxy extends abstract_service_1.AbstractProxy {
766
+ managerProxies = {};
767
+ constructor(context, service, cacheScope) {
768
+ super(context, service, 2, cacheScope ?? `${NAME}:${service.NS}:`); //! use `env.NAME` as default cache scope.
769
+ }
770
+ /**
771
+ * check if in mocks mode.
772
+ * - override this to set mocks.
773
+ *
774
+ * @returns boolean
775
+ */
776
+ isMocks() {
777
+ return false;
778
+ }
779
+ /**
780
+ * register this.
781
+ */
782
+ register(mgr) {
783
+ const res = super.register(mgr);
784
+ // also add into proxy lookup.
785
+ this.addManagerProxy(mgr.$mgr.type, mgr);
786
+ return res;
787
+ }
788
+ /**
789
+ * add into proxy lookup.
790
+ */
791
+ addManagerProxy(modelType, proxy) {
792
+ if (this.managerProxies[modelType]) {
793
+ throw Error(`@modelType[${modelType}] is invalid - already exists!`);
794
+ }
795
+ else {
796
+ this.managerProxies[modelType] = proxy;
797
+ }
798
+ }
799
+ /**
800
+ * get from lookup table.
801
+ */
802
+ getManagerProxy(modelType) {
803
+ return this.managerProxies[modelType];
804
+ }
805
+ /**
806
+ * @override fetchIdentityAccess
807
+ * @deprecated not supported.
808
+ */
809
+ async fetchIdentityAccess(identityId, domain) {
810
+ const errScope = `fetchIdentityAccess(${identityId ?? ''}/${domain ?? ''})`;
811
+ throw new Error(`400 NOT SUPPORTED - ${errScope}`);
812
+ }
813
+ /**
814
+ * get the `identity-token` (as session) from current req-context.
815
+ * - 1차 호출(front에서) 에서는 이 세션이 꼭 필요함.
816
+ * - 2차 호출(protocol이용) 에서는 선택사항으로, 조절가능해야함! (#TODO - 이걸 어떻게 구불할것인가?)
817
+ * - 로컬) 로컬 개발에서는 시뮬레이션 방법은 #TODO
818
+ *
819
+ * @param options.throwable flag to throw (default true)
820
+ */
821
+ getCurrentSession(options) {
822
+ // const useSession = !$U.env('LOCAL_ACCOUNT') ? $U.env('USE_SESSION') : null;
823
+ const throwable = options?.throwable ?? true;
824
+ const normalize = options?.normalize ?? true;
825
+ const useSession = options?.useSession ?? true;
826
+ const LOCAL_ACCOUNT = commons_1.$T.S(options?.LOCAL_ACCOUNT ?? engine_1.$U.env('LOCAL_ACCOUNT'));
827
+ const USE_SESSION = commons_1.$T.S(options?.USE_SESSION ?? engine_1.$U.env('USE_SESSION'));
828
+ LOCAL_ACCOUNT && (0, engine_1._inf)(this.service.NS, `> LOCAL_ACCOUNT =`, LOCAL_ACCOUNT);
829
+ USE_SESSION && (0, engine_1._inf)(this.service.NS, `> use-session =`, useSession, USE_SESSION);
830
+ const context = (options?.context ?? this.context);
831
+ //* WARN - in case of local, no need to throw error.
832
+ //* v5: local mode is a runtime setting (`LOCAL_MODE=1` off Lambda), never from `context.domain` (B5 § 6.3).
833
+ const isLocal = engine_1.$U.isLocalMode();
834
+ // load the `identity-token` from json file.
835
+ const _load = (file) => {
836
+ const data = (0, tools_1.loadJsonSync)(file);
837
+ if (typeof data?.error === 'string')
838
+ throw new Error(`@file[${file}] is invalid, ${data.error}`);
839
+ if (data?.context?.identity)
840
+ return data?.context?.identity;
841
+ return data;
842
+ };
843
+ const _default = () => {
844
+ //INFO - `userAgent`가 있다면, identity 생성 프로세스가 제대로 실행된것으로 판단해도됨! @240620.
845
+ if (typeof context?.identity?.userAgent == 'string')
846
+ return context?.identity;
847
+ //TODO - 아래 코드는 개선의 여지가 있음(문제점???) @241210
848
+ // if (typeof (context?.identity as any)?.identityId == 'string') return context?.identity;
849
+ return null;
850
+ };
851
+ //* load the `identity-token` from context or local json file.
852
+ const _session = () => {
853
+ //INFO - `userAgent`가 있다면, identity 생성 프로세스가 제대로 실행된것으로 판단해도됨! @240620.
854
+ const $idt = context?.identity;
855
+ //* Local 이면서 meta 정보가 있다면, 그걸로 identity 재구성함 @251212.
856
+ if (isLocal && typeof $idt?.meta === 'string' && $idt?.meta.length > 0) {
857
+ const $jwt = engine_1.$U.jwt('');
858
+ return $jwt.decode(context?.identity?.meta, { complete: false });
859
+ }
860
+ if (typeof $idt?.userAgent == 'string') {
861
+ //NOTE - `express` 에서 넘어온 경우 `identityPoolId=null`으로 가능함. (but apiKey should be empty) @251031
862
+ const fromExpress = typeof $idt?.identityPoolId === 'undefined' || $idt?.identityPoolId === null;
863
+ if (!fromExpress || typeof $idt?.apiKey === 'string') {
864
+ return context?.identity;
865
+ }
866
+ }
867
+ //TODO - 아래 코드는 개선의 여지가 있음(문제점???) @241210
868
+ //* v5: `LOCAL_ACCOUNT` is honoured only in local mode (B5 § 6.3).
869
+ if (LOCAL_ACCOUNT && isLocal)
870
+ return _default();
871
+ if (useSession) {
872
+ // _inf(this.service.NS, `> use-session =`, useSession, USE_SESSION);
873
+ if (useSession === true || useSession === 'true' || useSession === '1') {
874
+ //* v5: a session file (`USE_SESSION=*.json`) is honoured only in local mode (B5 § 6.3).
875
+ if (isLocal && USE_SESSION?.endsWith('.json')) {
876
+ const $sess = _load(USE_SESSION);
877
+ (0, engine_1._inf)(this.service.NS, `> use-session-json =`, engine_1.$U.json($sess));
878
+ return $sess;
879
+ }
880
+ return _default();
881
+ }
882
+ else if (typeof useSession === 'string' || useSession) {
883
+ return _load(useSession);
884
+ }
885
+ return _default();
886
+ }
887
+ return _default();
888
+ };
889
+ const $sess = _session();
890
+ // _inf(this.service.NS, `> use-session =`, $U.json($ses));
891
+ if (throwable && !isLocal) {
892
+ const eCode = `403 IDENTITY-TOKEN`;
893
+ const err = $sess?.error
894
+ ? `error:${$sess?.error}`
895
+ : $sess?.meta
896
+ ? `meta:${typeof $sess?.meta == 'string' ? $sess?.meta : engine_1.$U.json($sess?.meta)}`
897
+ : 'check x-lemon-identity';
898
+ //* validate conditions..
899
+ if (!$sess)
900
+ throw new Error(`${eCode} - .identity (IdentityToken) is required - context:${engine_1.$U.json(context)}`);
901
+ if ($sess?.sid === undefined)
902
+ throw new Error(`${eCode} - ${err}`);
903
+ if (!$sess?.sid)
904
+ throw new Error(`${eCode} - .sid (site-id) is required!`);
905
+ if (!$sess?.uid)
906
+ throw new Error(`${eCode} - .uid (user-id) is required!`);
907
+ }
908
+ //* normalize for the internal.
909
+ const sid = normalize && $sess?.sid?.startsWith('#') ? null : commons_1.$T.S2($sess?.sid);
910
+ const uid = normalize && $sess?.uid?.startsWith('#') ? null : commons_1.$T.S2($sess?.uid);
911
+ //* returns.
912
+ return $sess ? { ...$sess, sid, uid } : null;
913
+ }
914
+ /**
915
+ * find the current identity-id from context.
916
+ * - or, use env[LOCAL_ACCOUNT]
917
+ */
918
+ asCurrentIdentityId = (context) => {
919
+ context = context ?? this.context;
920
+ //* v5: `LOCAL_ACCOUNT` fallback only in local mode, not from `context.domain` (B5 § 6.3).
921
+ if (engine_1.$U.isLocalMode() && !context?.identity?.identityId) {
922
+ const LOCAL_ACCOUNT = engine_1.$U.env('LOCAL_ACCOUNT', '');
923
+ return LOCAL_ACCOUNT ? LOCAL_ACCOUNT : null;
924
+ }
925
+ return context && (0, helpers_1.getIdentityId)(context);
926
+ };
927
+ /**
928
+ * assert context condition.
929
+ */
930
+ assertContext(params,
931
+ /** (optional) */
932
+ options) {
933
+ const context = options?.context ?? this.context;
934
+ const throwable = options?.throwable ?? true;
935
+ const errScope = options?.errScope ?? `assertContext()`;
936
+ const $idt = context?.identity;
937
+ const isS = (s) => typeof s === 'string' && s?.length > 0;
938
+ //* call in local (v5: runtime local mode only, never from `context.domain` — B5 § 6.3)
939
+ const isLocal = engine_1.$U.isLocalMode();
940
+ //* signed call with access-key
941
+ const isSigned = isS($idt?.accessKey);
942
+ //* authed by cognito provider.
943
+ const isAuthed = isS($idt?.identityProvider) && isS($idt?.identityId) && isS($idt?.identityPoolId);
944
+ //* check if has role
945
+ const _hasRole = (role) => {
946
+ const $sess = this.getCurrentSession({ throwable: false, context });
947
+ return $sess?.roles?.includes(role);
948
+ };
949
+ const errCode = `403 NOT ALLOWED`;
950
+ if (typeof params?.isLocal == 'boolean' && params?.isLocal != isLocal) {
951
+ if (throwable)
952
+ throw new Error(`${errCode} - invalid isLocal[${params?.isLocal}] @${errScope}`);
953
+ return false;
954
+ }
955
+ if (typeof params?.isSigned == 'boolean' && params?.isSigned != isSigned) {
956
+ if (throwable)
957
+ throw new Error(`${errCode} - invalid isSigned[${params?.isSigned}] @${errScope}`);
958
+ return false;
959
+ }
960
+ if (typeof params?.isAuthed == 'boolean' && params?.isAuthed != isAuthed) {
961
+ if (throwable)
962
+ throw new Error(`${errCode} - invalid isAuthed[${params?.isAuthed}] @${errScope}`);
963
+ return false;
964
+ }
965
+ if (typeof params?.hasRole == 'string' && !_hasRole(params?.hasRole)) {
966
+ if (throwable)
967
+ throw new Error(`${errCode} - invalid hasRole[${params?.hasRole}] @${errScope}`);
968
+ return false;
969
+ }
970
+ return true;
971
+ }
972
+ /**
973
+ * check if current context has `admin` access
974
+ * 1. run in local mode (`$U.isLocalMode()`, v5 — no longer `domain='localhost'`)
975
+ * 2. access via signed access-key
976
+ * 3. authorized cognito and has `admin` role in identity-token.
977
+ *
978
+ * @param role (default) admin
979
+ * @param options optionals
980
+ * @returns boolean
981
+ */
982
+ hasAdminRole(role, options) {
983
+ role = commons_1.$T.S2(role, 'admin');
984
+ const context = (options?.context ?? this.context);
985
+ const $option = { ...options, throwable: false };
986
+ if (this.assertContext({ isLocal: true }, $option))
987
+ return true;
988
+ if (this.assertContext({ isSigned: true, isAuthed: false }, $option))
989
+ return true;
990
+ if (this.assertContext({ isAuthed: true, hasRole: role }, $option))
991
+ return true;
992
+ //TODO 아래는 임시로 role에서 바로 체크하도록함! @251212/steve (샘픔의 session-dev.json를 이용할경우 admin 체크가 잘 안됨)
993
+ if (context?.identity?.roles?.includes(role))
994
+ return true;
995
+ return false;
996
+ }
997
+ /**
998
+ * prepare `next-context` w/ the customized { domain, userAgent }
999
+ *
1000
+ * @param identityId the target identity-id
1001
+ * @param options additional options.
1002
+ */
1003
+ asNextContext(identityId, options) {
1004
+ const $ctx = this.context;
1005
+ identityId = identityId ?? this.asCurrentIdentityId($ctx);
1006
+ const identity = options?.identity ?? $ctx.identity;
1007
+ const session = options?.session ?? this.getCurrentSession({ throwable: false });
1008
+ const $ctx2 = {
1009
+ ...$ctx,
1010
+ domain: options?.domain ?? $ctx.domain,
1011
+ userAgent: options?.userAgent ?? $ctx.userAgent,
1012
+ identity: {
1013
+ ...session,
1014
+ ...identity,
1015
+ identityId,
1016
+ },
1017
+ };
1018
+ return $ctx2;
1019
+ }
1020
+ /**
1021
+ * from `identity-token`, make base of model { sid, uid }
1022
+ *
1023
+ * @param proxy the current proxy.
1024
+ * @returns base model
1025
+ */
1026
+ async makeModelBase(model, options) {
1027
+ const useSession = options?.useSession ?? false;
1028
+ const throwable = options?.throwable ?? false;
1029
+ const base = model ?? {};
1030
+ // STEP.0 populate with current parent.
1031
+ if (options?.parent) {
1032
+ const parent = options.parent;
1033
+ base['sid'] = base['sid'] ?? parent?.sid;
1034
+ base['uid'] = base['uid'] ?? parent?.uid;
1035
+ }
1036
+ // STEP.1 use session if applicable
1037
+ if (useSession) {
1038
+ const parent = this.getCurrentSession({ throwable });
1039
+ base['sid'] = base['sid'] ?? parent?.sid;
1040
+ base['uid'] = base['uid'] ?? parent?.uid;
1041
+ }
1042
+ return base;
1043
+ }
1044
+ /**
1045
+ * Check if the user can perform a specific action on a model.
1046
+ * - override this method to implement custom logic.
1047
+ *
1048
+ * @param action The action to check.
1049
+ * @param model The model to check against. (.type is used to find the manager-proxy)
1050
+ * @param options Optional parameters for the check.
1051
+ * @returns True if the action is allowed, false otherwise.
1052
+ */
1053
+ canDo(action, model, options) {
1054
+ const type = model?.type;
1055
+ if (typeof type !== 'string')
1056
+ throw new Error(`400 INVALID - @model.type (string) is required!`);
1057
+ const $prx = this.getManagerProxy(type);
1058
+ if (!$prx)
1059
+ return false;
1060
+ return $prx.canDo(action, model, options);
1061
+ }
1062
+ }
1063
+ exports.MyCoreProxy = MyCoreProxy;
1064
+ /**
1065
+ * class: `MyProxy`
1066
+ * - common parent of manager-proxy.
1067
+ */
1068
+ class MyManagerProxy extends abstract_service_1.ManagerProxy {
1069
+ proxy;
1070
+ constructor(proxy, mgr) {
1071
+ super(proxy, mgr);
1072
+ this.proxy = proxy;
1073
+ }
1074
+ /**
1075
+ * @override the default `normal()` to support _name.
1076
+ */
1077
+ normal = (N) => Object.keys(N || {}).reduce((M, k) => {
1078
+ //NOTE - imporved of basic `normal()` in core not to ignore `_` properties.
1079
+ if (k == '_id' || !this.$mgr.FIELDS.includes(k))
1080
+ return M;
1081
+ const v = N[k];
1082
+ //! `null` 은 DynamoDB에서 비어있는 문자임.
1083
+ M[k] = v === null ? '' : v;
1084
+ return M;
1085
+ }, {});
1086
+ /**
1087
+ * pack the query parameter for list
1088
+ * - `param` has the string values only including '', '!'
1089
+ * - `model` has the valid data type. (number | string | ... )
1090
+ *
1091
+ * ```ts
1092
+ * let model = bodyToModel(param)
1093
+ * ```
1094
+ *
1095
+ * @param param the original query parameter.
1096
+ * @param model the transformed model from `param`.
1097
+ * @param options (optional)
1098
+ * @returns
1099
+ */
1100
+ packSearchParam(param, model, options) {
1101
+ const type = this.$mgr.type;
1102
+ const useSession = options?.useSession ?? false;
1103
+ const throwable = options?.throwable ?? true;
1104
+ const errScope = options?.errScope ?? `packSearchParam(${type})`;
1105
+ const isLocal = engine_1.$U.isLocalMode(); //* v5: drop the `sid` pin only in local mode (B5 § 6.3).
1106
+ const session = useSession ? this.proxy.getCurrentSession({ throwable }) : null;
1107
+ const _has = (name) => param && (typeof param[name] === 'string' || typeof param[name] === 'number');
1108
+ const _val = (name) => (param ? commons_1.$T.S2(param[name]) : undefined);
1109
+ const _hasAdm = () => this.proxy.hasAdminRole('admin');
1110
+ // parse params
1111
+ const sort = _has('sort') ? commons_1.$T.S2(param?.sort, '') : undefined;
1112
+ const $page = (0, commons_1.parsePaginateParam)({ ...param, sort }, { limit: options?.limit ?? 10, page: 0 });
1113
+ if ($page.page < 0)
1114
+ throw new Error(`@page[${$page.page}] is invalid - ${errScope}`);
1115
+ // extract filtering by properties.
1116
+ // WARN - has no internal properties like { sid, gid, uid }
1117
+ const $param = this.$mgr.FIELDS.reduce((M, k) => {
1118
+ const key = k;
1119
+ const val = model && model[key];
1120
+ const qry = param && param[k];
1121
+ //* high priorty to param for not transformed field.
1122
+ if (qry === '' || qry === '!')
1123
+ M[key] = qry;
1124
+ else if (val !== undefined)
1125
+ M[key] = val;
1126
+ return M;
1127
+ }, {});
1128
+ // use w/ `identity-token` (known as session)
1129
+ if (useSession && !isLocal) {
1130
+ if (_hasAdm()) {
1131
+ $param['sid'] = _has('sid') ? (_val('sid') === '0' ? session?.sid : _val('sid')) : session?.sid;
1132
+ }
1133
+ else {
1134
+ $param['sid'] = session?.sid;
1135
+ }
1136
+ //* use the current session's uid.
1137
+ if (_has('uid'))
1138
+ $param['uid'] = _val('uid') === '0' ? session?.uid : _val('uid');
1139
+ }
1140
+ else {
1141
+ if (_has('sid'))
1142
+ $param['sid'] = _val('sid');
1143
+ if (_has('uid'))
1144
+ $param['uid'] = _val('uid') === '0' ? session?.uid : _val('uid');
1145
+ }
1146
+ //* returns params.
1147
+ return { $page, $param };
1148
+ }
1149
+ /**
1150
+ * override this to validate request'body.
1151
+ * @param model model from request.
1152
+ * @param modelId (optional) modelId if to update the target model.
1153
+ * @return validated model to save.
1154
+ */
1155
+ async validateModel(model, modelId) {
1156
+ const $org = modelId ? await this.get(modelId, true) : null;
1157
+ const validated = await this.$mgr.validateModel(model, $org);
1158
+ return validated;
1159
+ }
1160
+ /**
1161
+ * callback before saving model.
1162
+ * - could change field before saving.
1163
+ *
1164
+ * @param model the last model to save.
1165
+ * @param $org (optional) original model (null if creating)
1166
+ */
1167
+ async onBeforeSave(model, $org) {
1168
+ return model;
1169
+ }
1170
+ /**
1171
+ * callback after saving model.
1172
+ * - could trigger the next step if created.
1173
+ *
1174
+ * @param model the saved model (null if deleted)
1175
+ * @param $org (optional) original model (null if new created)
1176
+ */
1177
+ async onAfterSave(model, $org) {
1178
+ return model;
1179
+ }
1180
+ /**
1181
+ * generate the next id
1182
+ */
1183
+ async nextId(step) {
1184
+ return this.$mgr.nextId(step).then(n => commons_1.$T.S2(n));
1185
+ }
1186
+ /**
1187
+ * create new model
1188
+ * - use `.id` or `nextId()` for model-id
1189
+ * - validate model in seperate way before using this.
1190
+ *
1191
+ * @param model creation model
1192
+ * @param validate (optional) flag to validate (default is true)
1193
+ */
1194
+ async makeModel(model, validate) {
1195
+ const errScope = `makeModel(${this.$mgr.type}/${model?.id ?? ''})`;
1196
+ if (!model)
1197
+ throw new Error(`@model (${this.$mgr.type}-model) is required - ${errScope}`);
1198
+ validate = validate ?? true;
1199
+ // use the custome id or new generated id.
1200
+ const hasId = !!model.id;
1201
+ const modelId = hasId ? commons_1.$T.S2(model.id) : await this.nextId();
1202
+ // make default model as base.
1203
+ const $def0 = this.$mgr.buildDefault(modelId);
1204
+ const $def2 = this.override($def0, model); //! 초기생성(INSERT)에 대부분의 필드값 채워주기.
1205
+ const $def3 = validate ? await this.validateModel({ ...$def2, id: modelId }, null) : $def2;
1206
+ // overwrite (or update) the model.
1207
+ const $org = await this.get(modelId, { ...$def3 });
1208
+ if ($org?.id !== modelId)
1209
+ throw new Error(`.id[${model?.id}] is invalid (mismatched ${modelId}) - ${errScope}`);
1210
+ const $res = hasId ? this.override($org, $def3) : $org;
1211
+ return $res; //! return the model with id.
1212
+ }
1213
+ /**
1214
+ * Check if the current user can perform the specified action on the given model.
1215
+ * - override this method to implement custom logic.
1216
+ *
1217
+ * @param action The action to check.
1218
+ * @param model The model to check against.
1219
+ * @param options Additional options for the check.
1220
+ * @returns True if the action is allowed, false otherwise.
1221
+ */
1222
+ canDo(action, model, options) {
1223
+ const errScope = options?.errScope ?? `canDo(${action ?? ''}:${this.$mgr.type}/${model?.id ?? '-'})`;
1224
+ const useSession = options?.useSession ?? this.proxy.service.isUseSession();
1225
+ const throwable = options?.throwable ?? true;
1226
+ const context = options?.context;
1227
+ // STEP.0 validate parameters.
1228
+ const _act = commons_1.$T.asLut(action, $LUT.CanDoAction, { throwable: false, default: null });
1229
+ if (!_act || typeof _act !== 'string')
1230
+ throw new Error(`400 INVALID - action[${action ?? ''}] is invalid @${errScope}`);
1231
+ // no session but required.
1232
+ const $sess = this.proxy.getCurrentSession({ throwable, useSession, context });
1233
+ if (useSession && !$sess)
1234
+ return false;
1235
+ // admin can do anything.
1236
+ const isAdmin = this.proxy.hasAdminRole(null, { errScope, context });
1237
+ const hasRole = $sess?.roles?.length > 0 ? true : false; // guest if empty.
1238
+ if (isAdmin)
1239
+ return true;
1240
+ // session & model information.
1241
+ const S = { sid: commons_1.$T.S2($sess?.sid), uid: commons_1.$T.S2($sess?.uid) };
1242
+ const M = { sid: commons_1.$T.S2(model?.sid), uid: commons_1.$T.S2(model?.uid) };
1243
+ // check action.
1244
+ if (hasRole) {
1245
+ switch (_act) {
1246
+ case 'list':
1247
+ case 'read':
1248
+ case 'create':
1249
+ // (default) action is allowed if sid matches or no sid.
1250
+ if (!M.sid)
1251
+ return true;
1252
+ if (S.sid === M.sid)
1253
+ return true;
1254
+ break;
1255
+ case 'update':
1256
+ case 'delete':
1257
+ // (default) action is allowed if sid&uid matches.
1258
+ //* v5: a `sid`-less record is admin-only for update/delete (B5 § 6.4).
1259
+ if (!M.sid)
1260
+ break;
1261
+ if (S.sid === M.sid && S.uid === M.uid)
1262
+ return true;
1263
+ break;
1264
+ default:
1265
+ throw new Error(`400 INVALID - action[${_act}] is invalid @${errScope}`);
1266
+ }
1267
+ }
1268
+ // not allowed.
1269
+ if (throwable)
1270
+ throw new Error(`403 NOT ALLOWED - action[${_act}] is invalid @${errScope}`);
1271
+ return false;
1272
+ }
1273
+ /**
1274
+ * read multiple nodes.
1275
+ * @param ids list of object-ids
1276
+ * @param throwable (optional) flag to throw error if not found
1277
+ */
1278
+ async mget(ids, options) {
1279
+ const throwable = typeof options === 'boolean' ? options : options?.throwable ?? true;
1280
+ if (!ids || ids.length === 0)
1281
+ return [];
1282
+ const result = [];
1283
+ const toFetch = [];
1284
+ const indexMap = {};
1285
+ //* STEP.1 check cached items
1286
+ ids.forEach((id, index) => {
1287
+ const err404 = `404 NOT FOUND - proxy/${this.$mgr.type}/id:${id}`;
1288
+ //* already marked as null (404)
1289
+ if (this._org[id] === null) {
1290
+ if (throwable)
1291
+ throw new Error(err404);
1292
+ result[index] = null;
1293
+ return;
1294
+ }
1295
+ //* found in _new
1296
+ const N = this._new[id];
1297
+ if (N !== undefined) {
1298
+ result[index] = N;
1299
+ return;
1300
+ }
1301
+ //* need to fetch
1302
+ if (!indexMap[id]) {
1303
+ indexMap[id] = [];
1304
+ toFetch.push(id);
1305
+ }
1306
+ indexMap[id].push(index);
1307
+ });
1308
+ //* STEP.2 batch read from storage
1309
+ if (toFetch.length > 0) {
1310
+ const context = this.proxy.asNextContext();
1311
+ //TODO [Steve] 여기서 갑자기 storage 에서 읽어오는부분을 proxy 에서 하도록 개선 필요해 보임 @260608.
1312
+ const $res = await this.$mgr.storage.storage.doReadMulti(this.$mgr.type, toFetch, { throwable, context });
1313
+ const successMap = {};
1314
+ ($res?.success || []).forEach(model => {
1315
+ const id = model.id;
1316
+ if (id)
1317
+ successMap[id] = model;
1318
+ });
1319
+ //* STEP.3 save to cache and build result
1320
+ toFetch.forEach(id => {
1321
+ const M = successMap[id];
1322
+ if (M) {
1323
+ const M2 = this.normal(M);
1324
+ this._org[id] = M2;
1325
+ const M3 = JSON.parse(engine_1.$U.json(M2));
1326
+ this._new[id] = M3;
1327
+ indexMap[id].forEach(index => {
1328
+ result[index] = M3;
1329
+ });
1330
+ }
1331
+ else {
1332
+ this._org[id] = null;
1333
+ const err404 = `404 NOT FOUND - proxy/${this.$mgr.type}/id:${id}`;
1334
+ indexMap[id].forEach(index => {
1335
+ if (throwable)
1336
+ throw new Error(err404);
1337
+ result[index] = null;
1338
+ });
1339
+ }
1340
+ });
1341
+ }
1342
+ return result;
1343
+ }
1344
+ /**
1345
+ * update multiple nodes.
1346
+ * @param updates list of models (should include id)
1347
+ */
1348
+ async mset(updates) {
1349
+ if (!updates || updates.length === 0)
1350
+ return [];
1351
+ const result = [];
1352
+ for (const model of updates) {
1353
+ const id = model.id;
1354
+ if (!id)
1355
+ throw new Error(`@id is required - proxy/${this.$mgr.type}/mset()!`);
1356
+ const O = await this.get(id);
1357
+ result.push(this.override(O, model));
1358
+ }
1359
+ return result;
1360
+ }
1361
+ }
1362
+ exports.MyManagerProxy = MyManagerProxy;
1363
+ /**
1364
+ * class: `OS2Service`
1365
+ * - Master 권한 전용 Elasticsearch 관리 서비스
1366
+ * - service for OS2: data path via FGAC (`$fgac`), index management via the master client.
1367
+ */
1368
+ class OS2Service extends cores_1.OS2CoreService {
1369
+ $fgac;
1370
+ /**
1371
+ * constructor (no client here; the master client is created on first `client` access)
1372
+ */
1373
+ constructor(options, fgac) {
1374
+ super(options);
1375
+ this.$fgac = fgac;
1376
+ }
1377
+ /**
1378
+ * build the master client: `auth` is read from config now (after engine init), and set only when both exist.
1379
+ */
1380
+ createClient() {
1381
+ const _config = (n) => cores_1.default.config.config.get(n);
1382
+ const username = _config('OS2_MASTER_UID');
1383
+ const password = _config('OS2_MASTER_PWD');
1384
+ const auth = username && password ? { auth: { username, password } } : {};
1385
+ //* same node/ssl as the base; the package is loaded here, on first use.
1386
+ return this.newClient(auth);
1387
+ }
1388
+ //* v5 B3: `describe`/`createIndex`/`search` are inherited from `OS2CoreService` (identical-body overrides removed).
1389
+ /**
1390
+ * delete index by name
1391
+ */
1392
+ async deleteIndex(indexName) {
1393
+ (0, engine_1._log)(NS, `- deleteIndex(${indexName})`);
1394
+ const client = this.client;
1395
+ const res = await client.indices.delete({ index: indexName }).catch(cores_1.$ERROR.handler('delete', e => {
1396
+ const msg = (0, test_helper_1.GETERR)(e);
1397
+ if (msg.startsWith('404 INDEX NOT FOUND'))
1398
+ throw new Error(`404 NOT FOUND - index:${indexName}`);
1399
+ throw e;
1400
+ }));
1401
+ return res;
1402
+ }
1403
+ /**
1404
+ * save single item
1405
+ *
1406
+ * @param id - id
1407
+ * @param item - item to save
1408
+ * @param type - document type (default: doc-type given at construction time)
1409
+ */
1410
+ async saveItem(id, item) {
1411
+ const { indexName, idName } = this.options;
1412
+ (0, engine_1._log)(NS, `- saveItem(${id})`);
1413
+ // const { client } = instance(endpoint);
1414
+ // prepare item body and autocomplete fields
1415
+ const body = { ...item, [idName]: id };
1416
+ const body2 = this.popullateAutocompleteFields(body);
1417
+ const params = { index: indexName, id, body: body2 };
1418
+ if (idName === '_id')
1419
+ delete params.body[idName]; //WARN! `_id` is reserved in ES6.
1420
+ (0, engine_1._log)(NS, `> params[${id}] =`, engine_1.$U.json(params));
1421
+ //NOTE - use npm `elasticsearch#13.2.0` for avoiding error.
1422
+ const res = await this.$fgac.doFgacCreate(params).catch(e => {
1423
+ const msg = (0, test_helper_1.GETERR)(e);
1424
+ if (msg.startsWith('409 VERSION CONFLICT ENGINE') || msg.startsWith('409 Conflict')) {
1425
+ const param2 = { index: indexName, id, body: { ...body2 } };
1426
+ return this.$fgac.doFgacSave(param2);
1427
+ }
1428
+ throw e;
1429
+ });
1430
+ (0, engine_1._log)(NS, `> create[${id}].res =`, engine_1.$U.json({ ...res, meta: undefined }));
1431
+ const _version = engine_1.$U.N(res?._version, 0);
1432
+ const _id = res?._id;
1433
+ const res2 = { ...body, _id, _version };
1434
+ return res2;
1435
+ }
1436
+ /**
1437
+ * bulk save multiple items using elasticsearch client
1438
+ *
1439
+ * @param items - array of items to save
1440
+ */
1441
+ async bulkItems(items, options) {
1442
+ const errScope = `bulkItems(${this.options.indexName})`;
1443
+ const { indexName } = this.options;
1444
+ //* FGAC only: do not touch `this.client` here (it would create the master client).
1445
+ const idName = options?.idName ?? '_id';
1446
+ if (!Array.isArray(items) || items.length === 0)
1447
+ throw new Error(`@items (array) is required - length=${items?.length ?? 'null'} - ${errScope}`);
1448
+ const { failedItems, bulkBody } = items.reduce((acc, item) => {
1449
+ const id = item[idName];
1450
+ if (!id) {
1451
+ acc.failedItems.push({
1452
+ reason: `@${idName} is invalid - ${errScope}`,
1453
+ item,
1454
+ });
1455
+ return acc;
1456
+ }
1457
+ const body = { ...item, [idName]: id };
1458
+ const body2 = this.popullateAutocompleteFields(body);
1459
+ if (idName === '_id')
1460
+ delete body2['_id']; // Remove reserved _id field
1461
+ acc.bulkBody.push({ index: { _index: indexName, _id: id } });
1462
+ acc.bulkBody.push(body2);
1463
+ return acc;
1464
+ }, {
1465
+ failedItems: [],
1466
+ bulkBody: [],
1467
+ });
1468
+ if (bulkBody.length === 0)
1469
+ return { success: 0, failed: failedItems.length, failedItems };
1470
+ const res = await this.$fgac.doFgacBulk({ index: indexName, body: bulkBody }).catch(cores_1.$ERROR.handler('bulk', e => {
1471
+ (0, engine_1._err)(NS, `> bulkItems[${indexName}].err =`, e);
1472
+ throw e;
1473
+ }));
1474
+ // const res = await client.bulk({ body: bulkBody }).catch(
1475
+ // $ERROR.handler('bulk', e => {
1476
+ // _err(NS, `> bulkItems[${indexName}].err =`, e);
1477
+ // throw e;
1478
+ // }),
1479
+ // );
1480
+ const itemsRes = res?.items ?? [];
1481
+ const failedFromBulk = itemsRes
1482
+ .filter((r) => r.index?.error)
1483
+ .map((r) => ({
1484
+ reason: r.index.error?.reason || 'unknown error',
1485
+ item: r.index,
1486
+ }));
1487
+ failedItems.push(...failedFromBulk);
1488
+ const failed = failedItems.length;
1489
+ const success = itemsRes.length - failed;
1490
+ (0, engine_1._log)(NS, `> bulkItems done: success=${success}, failed=${failed},`);
1491
+ return { success, failed, failedItems };
1492
+ }
1493
+ /**
1494
+ * push item for time-series data (via FGAC; the result is the response body itself).
1495
+ *
1496
+ * @param item - item to push
1497
+ */
1498
+ async pushItem(item) {
1499
+ const { indexName } = this.options;
1500
+ const id = item.id ?? '';
1501
+ const body = { ...item };
1502
+ const body2 = this.popullateAutocompleteFields(body);
1503
+ (0, engine_1._log)(NS, `- pushItem(${id})`);
1504
+ const params = { index: indexName, body: body2, id };
1505
+ (0, engine_1._log)(NS, `> params[${id}] =`, engine_1.$U.json(params));
1506
+ //NOTE - use npm `elasticsearch#13.2.0` for avoiding error.
1507
+ // const { client } = instance(endpoint);
1508
+ const res = await this.$fgac.doFgacSave(params).catch(
1509
+ // $ERROR.throwAsJson,
1510
+ cores_1.$ERROR.handler('index', e => {
1511
+ (0, engine_1._err)(NS, `> index[${indexName}].err =`, e instanceof Error ? e : engine_1.$U.json(e));
1512
+ throw e;
1513
+ }));
1514
+ // {"_index":"test-v3","_type":"_doc","_id":"rTeHiW4BPb_liACrA9qa","_version":1,"result":"created","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":2,"_primary_term":1}
1515
+ (0, engine_1._log)(NS, `> create[${id}].res =`, engine_1.$U.json({ ...res, meta: undefined }));
1516
+ //* fixed: the FGAC result is the body itself (v4 read `res.body?._id`, always undefined)
1517
+ const _id = res?._id;
1518
+ const _version = res?._version;
1519
+ const res2 = { ...body, _id, _version };
1520
+ return res2;
1521
+ }
1522
+ /**
1523
+ * read item with projections
1524
+ *
1525
+ * @param id - item-id
1526
+ * @param views - projections
1527
+ */
1528
+ async readItem(id, views) {
1529
+ const { indexName } = this.options;
1530
+ (0, engine_1._log)(NS, `- readItem(${id})`);
1531
+ const params = { index: indexName, id };
1532
+ if (views) {
1533
+ const fields = [];
1534
+ const keys = Array.isArray(views) ? views : Object.keys(views);
1535
+ keys.forEach((k) => {
1536
+ fields.push(k);
1537
+ });
1538
+ params._source = fields;
1539
+ }
1540
+ (0, engine_1._log)(NS, `> params[${id}] =`, engine_1.$U.json(params));
1541
+ // const { client } = instance(endpoint);
1542
+ const res = await this.$fgac.doFgacRead(params).catch(
1543
+ // $ERROR.throwAsJson,
1544
+ cores_1.$ERROR.handler('read', e => {
1545
+ const msg = (0, test_helper_1.GETERR)(e);
1546
+ if (msg.startsWith('404 NOT FOUND'))
1547
+ throw new Error(`404 NOT FOUND - id:${id}`);
1548
+ if (msg.startsWith('404 INDEX NOT FOUND'))
1549
+ throw new Error(`404 NOT FOUND - index:${indexName}`);
1550
+ throw e;
1551
+ }));
1552
+ (0, engine_1._log)(NS, `> read[${id}].res =`, engine_1.$U.json({ ...res, meta: undefined }));
1553
+ const _id = res._id;
1554
+ const _version = res._version;
1555
+ const data = res?._source || res._source || {};
1556
+ // delete internal (analyzed) field
1557
+ delete data[cores_1.OS2CoreService.DECOMPOSED_FIELD];
1558
+ delete data[cores_1.OS2CoreService.QWERTY_FIELD];
1559
+ const res2 = { ...data, _id, _version };
1560
+ return res2;
1561
+ }
1562
+ /**
1563
+ * delete item with projections
1564
+ *
1565
+ * @param id - item-id
1566
+ */
1567
+ async deleteItem(id) {
1568
+ const { indexName } = this.options;
1569
+ (0, engine_1._log)(NS, `- deleteItem(${id})`);
1570
+ const params = { index: indexName, id, body: {} };
1571
+ (0, engine_1._log)(NS, `> params[${id}] =`, engine_1.$U.json(params));
1572
+ // const { client } = instance(endpoint);
1573
+ const res = await this.$fgac.doFgacDelete(params).catch(cores_1.$ERROR.handler('delete', e => {
1574
+ const msg = (0, test_helper_1.GETERR)(e);
1575
+ if (msg.startsWith('404 NOT FOUND'))
1576
+ throw new Error(`404 NOT FOUND - id:${id}`);
1577
+ if (msg.startsWith('404 INDEX NOT FOUND'))
1578
+ throw new Error(`404 NOT FOUND - index:${indexName}`);
1579
+ throw e;
1580
+ }));
1581
+ (0, engine_1._log)(NS, `> delete[${id}].res =`, engine_1.$U.json({ ...res, meta: undefined }));
1582
+ const _id = res._id;
1583
+ const _version = res._version;
1584
+ const data = res._source || res._source || {};
1585
+ const res2 = { ...data, _id, _version };
1586
+ return res2;
1587
+ }
1588
+ /**
1589
+ * update item (throw if not exist)
1590
+ * `update table set a=1, b=b+2 where id='a1'`
1591
+ * 0. no of `a1` -> 1,2 (created)
1592
+ * 1. a,b := 10,20 -> 11,22
1593
+ * 2. a,b := 10,null -> 11,2 (upsert)
1594
+ * 3. a,b := null,20 -> 1,22
1595
+ *
1596
+ * @param id - item-id
1597
+ * @param item - item to update
1598
+ * @param increments - item to increase
1599
+ * @param options - (optional) request option of client.
1600
+ */
1601
+ async updateItem(id, item, increments, options) {
1602
+ const errScope = `updateItem(${id ?? ''})`;
1603
+ if (increments)
1604
+ throw new Error(`400 NOT SUPPORTED - invalid increments[${engine_1.$U.json(increments)}] @${errScope}`);
1605
+ // return this.saveItem(id, item);
1606
+ //NOTE - 이후 saveItem() 호출을 위한 tricky solution.
1607
+ throw new Error(`404 NOT FOUND - ${errScope}`);
1608
+ }
1609
+ /**
1610
+ * run search and get the raw response.
1611
+ * @param body - Elasticsearch Query DSL that defines the search request (e.g., size, query, filters).
1612
+ * @param searchType - type of search (e.g., 'query_then_fetch', 'dfs_query_then_fetch').
1613
+ */
1614
+ async searchRaw(body, searchType) {
1615
+ if (!body)
1616
+ throw new Error('@body (SearchBody) is required');
1617
+ const { indexName } = this.options;
1618
+ (0, engine_1._log)(NS, `- search(${indexName}, ${searchType || ''})....`);
1619
+ (0, engine_1._log)(NS, `> body =`, engine_1.$U.json(body));
1620
+ const params = { index: indexName, body };
1621
+ if (searchType)
1622
+ params.search_type = searchType;
1623
+ (0, engine_1._log)(NS, `> params =`, engine_1.$U.json({ ...params, body: undefined }));
1624
+ const $res = await this.$fgac.doFgacSearch(params).catch(cores_1.$ERROR.handler('search', e => {
1625
+ (0, engine_1._err)(NS, `> search[${indexName}].err =`, e);
1626
+ throw e;
1627
+ }));
1628
+ //* return raw results.
1629
+ return $res;
1630
+ }
1631
+ }
1632
+ exports.OS2Service = OS2Service;
1633
+ /**
1634
+ * type: `OS2FGACManager`
1635
+ * - FGAC(Fine Grained Access Controll) Proxy for OS2
1636
+ */
1637
+ class OS2FGACManager {
1638
+ _fgacProxy;
1639
+ options;
1640
+ constructor(options) {
1641
+ this.options = options;
1642
+ }
1643
+ hello = () => `FGACService`;
1644
+ get proxy() {
1645
+ const errScope = `OS2FGACManager(${this.options?.indexName ?? ''})`;
1646
+ if (!this._fgacProxy) {
1647
+ const endpoint = this.options.endpoint;
1648
+ const host = endpoint.split('://')[1];
1649
+ const $config = engine_1.$engine.module('config');
1650
+ const OS2_JWT_TOKEN = engine_1.$U.env('OS2_JWT_TOKEN', '');
1651
+ const message = engine_1.$U.env('OS2_MESSAGE', '');
1652
+ const signature = $config?.config?.get('OS2_SIGNATURE');
1653
+ const jwtToken = OS2_JWT_TOKEN !== '' ? OS2_JWT_TOKEN : `${message}.${signature}`; // if OS2_JWT_TOKEN is provided, use it
1654
+ const sigConfig = {
1655
+ accessKey: '**',
1656
+ secretKey: '**',
1657
+ region: 'ap-northeast-2',
1658
+ serviceName: 'es',
1659
+ host,
1660
+ };
1661
+ (0, engine_1._log)(NS, `> sigConfig =`, engine_1.$U.json({ ...sigConfig, secretKey: `${sigConfig?.secretKey ? '***' : ''}` }));
1662
+ const headers = {
1663
+ Authorization: `Bearer ${jwtToken}`,
1664
+ 'Content-Type': 'application/json',
1665
+ };
1666
+ this._fgacProxy = (0, helpers_1.createSigV4Proxy)('fgac', endpoint, sigConfig, { headers });
1667
+ }
1668
+ return this._fgacProxy;
1669
+ }
1670
+ /** FGAC 기반 검색 */
1671
+ async doFgacSearch(params) {
1672
+ const index = params?.index ?? '';
1673
+ const body = (0, commons_1.onlyDefined)(params?.body ?? undefined);
1674
+ const queryParams = params?.search_type ? { search_type: params?.search_type } : undefined;
1675
+ return this.proxy.doProxy('POST', index, `_search`, queryParams, body);
1676
+ }
1677
+ /** FGAC 기반 읽기: client.get() 대응 */
1678
+ async doFgacRead(params) {
1679
+ const index = params?.index ?? '';
1680
+ const id = encodeURIComponent(params?.id ?? '');
1681
+ const body = (0, commons_1.onlyDefined)(params?.body ?? undefined);
1682
+ return this.proxy.doProxy('GET', index, `_doc/${id}`, undefined, body);
1683
+ }
1684
+ /** FGAC 기반 저장: client.index() 대응 */
1685
+ async doFgacSave(params) {
1686
+ const index = params?.index ?? '';
1687
+ const id = encodeURIComponent(params?.id ?? '');
1688
+ const body = (0, commons_1.onlyDefined)(params?.body ?? undefined);
1689
+ return this.proxy.doProxy('PUT', index, `_doc/${id}`, undefined, body);
1690
+ }
1691
+ /** FGAC 기반 저장: client.index() 대응 */
1692
+ async doFgacBulk(bulk) {
1693
+ const index = bulk?.index ?? '';
1694
+ const body = bulk?.body ?? [];
1695
+ const errScope = `doFgacBulk(${index})`;
1696
+ if (!Array.isArray(body))
1697
+ throw new Error(`@body(array) is required - ${errScope}`);
1698
+ if (body.length === 0)
1699
+ return [];
1700
+ // 사전 포맷된 bulkBody를 JSONL 문자열로 변환
1701
+ const content = body.map(line => engine_1.$U.json(line)).join('\n') + '\n';
1702
+ return this.proxy.doProxy('POST', index, `_bulk`, undefined, content);
1703
+ }
1704
+ /** FGAC 기반 업데이트: client.update() 대응 */
1705
+ async doFgacUpdate(params) {
1706
+ const index = params?.index ?? '';
1707
+ const id = encodeURIComponent(params?.id ?? '');
1708
+ const body = (0, commons_1.onlyDefined)(params?.body ?? undefined);
1709
+ return this.proxy.doProxy('POST', index, `_update/${id}`, undefined, body);
1710
+ }
1711
+ /** FGAC 기반 삭제: client.delete() 대응 */
1712
+ async doFgacDelete(params) {
1713
+ const index = params?.index ?? '';
1714
+ const id = encodeURIComponent(params?.id ?? '');
1715
+ return this.proxy.doProxy('DELETE', index, `_doc/${id}`);
1716
+ }
1717
+ /** FGAC 기반 Create (Strict - 존재하면 실패): client.create() 대응 */
1718
+ async doFgacCreate(params) {
1719
+ const index = params?.index ?? '';
1720
+ const id = encodeURIComponent(params?.id ?? '');
1721
+ const body = (0, commons_1.onlyDefined)(params?.body ?? undefined);
1722
+ return this.proxy.doProxy('PUT', index, `_doc/${id}`, { op_type: 'create' }, body);
1723
+ }
1724
+ /** FGAC 기반 인덱스 삭제: client.indices.delete() 대응 */
1725
+ async doFgacDeleteIndex(index) {
1726
+ return this.proxy.doProxy('DELETE', index, '');
1727
+ }
1728
+ /**
1729
+ * FGAC 기반 인덱스 설명: client.indices.describe() 대응
1730
+ */
1731
+ async doFgacDescribe(index) {
1732
+ const errScope = `doFgacDescribe(${index})`;
1733
+ if (!index)
1734
+ throw new Error(`.index(string) is required - ${errScope}`);
1735
+ // 1) 설정 조회
1736
+ const resSettings = await this.doFgacGetSettings(index);
1737
+ // resSettings.body 안에 { [index]: { settings: { … } } } 형태로 들어옴
1738
+ // 2) 매핑 조회
1739
+ const resMappings = await this.doFgacGetMapping(index);
1740
+ // resMappings.body 안에 { [index]: { mappings: { … } } } 형태로 들어옴
1741
+ return {
1742
+ settings: resSettings[index]?.settings || {},
1743
+ mappings: resMappings[index]?.mappings || {},
1744
+ };
1745
+ }
1746
+ /**
1747
+ * FGAC 기반 인덱스 Settings 변경: client.indices.putSettings() 대응
1748
+ */
1749
+ async doFgacPutSettings(index, settings) {
1750
+ const errScope = `doFgacPutSettings(${index})`;
1751
+ if (!index)
1752
+ throw new Error(`.index(string) is required - ${errScope}`);
1753
+ return this.proxy.doProxy('PUT', index, `_settings`, undefined, { settings });
1754
+ }
1755
+ /**
1756
+ * FGAC 기반 인덱스 Settings 조회: client.indices.getSettings() 대응
1757
+ */
1758
+ async doFgacGetSettings(index) {
1759
+ const errScope = `doFgacGetSettings(${index})`;
1760
+ if (!index)
1761
+ throw new Error(`.index(string) is required - ${errScope}`);
1762
+ return this.proxy.doProxy('GET', index, `_settings`);
1763
+ }
1764
+ /**
1765
+ * FGAC 기반 인덱스 Mapping 조회: client.indices.getMapping() 대응
1766
+ */
1767
+ async doFgacGetMapping(index) {
1768
+ const errScope = `doFgacGetMapping(${index})`;
1769
+ if (!index)
1770
+ throw new Error(`.index(string) is required - ${errScope}`);
1771
+ return this.proxy.doProxy('GET', index, `_mapping`);
1772
+ }
1773
+ /**
1774
+ * FGAC 기반 인덱스 Mapping 변경: client.indices.putMapping() 대응
1775
+ */
1776
+ async doFgacPutMapping(index, mapping) {
1777
+ const errScope = `doFgacPutMapping(${index})`;
1778
+ if (!index)
1779
+ throw new Error(`.index(string) is required - ${errScope}`);
1780
+ return this.proxy.doProxy('PUT', index, `_mapping`, undefined, mapping);
1781
+ }
1782
+ /**
1783
+ * FGAC 기반 인덱스 닫기: client.indices.close() 대응
1784
+ */
1785
+ async doFgacCloseIndex(index) {
1786
+ const errScope = `doFgacCloseIndex(${index})`;
1787
+ if (!index)
1788
+ throw new Error(`.index(string) is required - ${errScope}`);
1789
+ return this.proxy.doProxy('POST', index, `_close`);
1790
+ }
1791
+ /**
1792
+ * FGAC 기반 인덱스 열기: client.indices.open() 대응
1793
+ */
1794
+ async doFgacOpenIndex(index) {
1795
+ const errScope = `doFgacOpenIndex(${index})`;
1796
+ if (!index)
1797
+ throw new Error(`.index(string) is required - ${errScope}`);
1798
+ return this.proxy.doProxy('POST', index, `_open`);
1799
+ }
1800
+ }
1801
+ exports.OS2FGACManager = OS2FGACManager;
1802
+ /**
1803
+ * OS2 인스턴스
1804
+ * - describeIndex(), destroyIndex(), search() with elastic initialized by master-key
1805
+ * - one service set (fgac, elastic, query, synchronizer) only when `endpoint && indexName`.
1806
+ */
1807
+ class OS2Instance extends abstract_service_1.AbstractOS2Instance {
1808
+ fgac;
1809
+ /**
1810
+ * build one service set when `endpoint && indexName` (no client: it is created on first use).
1811
+ */
1812
+ constructor({ endpoint, indexName, idName, tableName, autocompleteFields, roleName, userName, }) {
1813
+ super();
1814
+ const options = (0, commons_1.onlyDefined)({
1815
+ endpoint,
1816
+ indexName,
1817
+ autocompleteFields,
1818
+ roleName,
1819
+ userName,
1820
+ idName: idName === null ? undefined : idName ?? '$id',
1821
+ });
1822
+ if (endpoint && indexName) {
1823
+ this.fgac = new OS2FGACManager(options);
1824
+ this.elastic = new OS2Service(options, this.fgac);
1825
+ this.query = new cores_1.OS2QueryService(this.elastic);
1826
+ this.synchronizer = new abstract_service_1.OS2Synchronizer(this.elastic, { tableName });
1827
+ }
1828
+ }
1829
+ hello = () => `OS2Instance`;
1830
+ //* v5 B3: `describeIndex` is inherited from `AbstractOS2Instance` (identical-body override removed).
1831
+ //* kept (unlike the removed identical overrides): it narrows the return type to `OS2Option`.
1832
+ get options() {
1833
+ if (!this.elastic)
1834
+ return null;
1835
+ return this.elastic.options;
1836
+ }
1837
+ get $X2() {
1838
+ return $X2;
1839
+ }
1840
+ /**
1841
+ * search
1842
+ */
1843
+ async search(body, params) {
1844
+ if (!this.elastic)
1845
+ throw new Error(`Could not read Elasticsearch endpoint or index setting.`);
1846
+ const searchType = params?.searchType;
1847
+ const elastic = this.elastic;
1848
+ return elastic.search(body, searchType);
1849
+ }
1850
+ /** synchronize dynamo contents to os2 */
1851
+ async synchronizeContents(options) {
1852
+ const stopOnError = options?.stopOnError ?? false;
1853
+ const filter = null;
1854
+ // STEP 1. get source items
1855
+ const dynamoOptions = {
1856
+ tableName: engine_1.$U.env('MY_DYNAMO_TABLE', ''),
1857
+ idName: '_id',
1858
+ };
1859
+ const $scan = new cores_2.DynamoScanService(dynamoOptions);
1860
+ const { limit } = (0, commons_1.parsePaginateParam)(options);
1861
+ // STEP 2. get role model
1862
+ const indexName = this.options.indexName;
1863
+ //* runs in parallel
1864
+ const _parrallel = async (list, $res) => {
1865
+ const items = await (0, helpers_1.my_parrallel)(list, async (N) => {
1866
+ const id = N._id ?? N.id;
1867
+ try {
1868
+ const stereo = commons_1.$T.S2(N?.stereo);
1869
+ if (!stereo || !stereo?.startsWith('#')) {
1870
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1871
+ const { _id, ...rest } = N;
1872
+ await this.fgac.doFgacSave({ index: indexName, id, body: rest });
1873
+ return { id, error: null };
1874
+ }
1875
+ return { id };
1876
+ }
1877
+ catch (error) {
1878
+ if (stopOnError && $res) {
1879
+ (0, engine_1._err)(NS, `> error[${id}] =`, error);
1880
+ $res.last = null;
1881
+ }
1882
+ return { id, error: (0, test_helper_1.GETERR)(error) };
1883
+ }
1884
+ });
1885
+ return items;
1886
+ };
1887
+ // runs with bulk
1888
+ const _bulks = async (list, $res) => {
1889
+ const body = list
1890
+ .map(N => {
1891
+ const _id = N._id ?? N.id;
1892
+ const stereo = commons_1.$T.S2(N?.stereo);
1893
+ if (!stereo || !stereo?.startsWith('#')) {
1894
+ return { ...N, _id };
1895
+ }
1896
+ return null;
1897
+ })
1898
+ .filter(N => !!N);
1899
+ // not supported autocomplete options
1900
+ const items = await this.fgac.doFgacBulk({ index: indexName, body });
1901
+ return items?.map((N) => N?.index ?? N?.create);
1902
+ };
1903
+ // STEP 3. migrate items using doFgacSave
1904
+ let last = null;
1905
+ const results = [];
1906
+ const _preprocessItems = (items) => {
1907
+ return items.filter(item => {
1908
+ const stereo = commons_1.$T.S2(item?.stereo);
1909
+ return !(stereo && stereo.startsWith('#'));
1910
+ });
1911
+ };
1912
+ while (true) {
1913
+ if (last)
1914
+ (0, engine_1._inf)(NS, `> last =`, engine_1.$U.json(last));
1915
+ const $res = await $scan.scan(limit, last, filter);
1916
+ const list = _preprocessItems($res?.list) ?? [];
1917
+ // const items = await _parrallel(list, $res);
1918
+ const res = await this.bulkItems(list, { idName: '_id' });
1919
+ const failedItems = res?.failedItems ?? [];
1920
+ (0, engine_1._log)(NS, `>> len =`, failedItems?.length ?? '-');
1921
+ // 실패 ID + 메시지를 Map으로 생성
1922
+ const failedMap = failedItems.reduce((map, f) => {
1923
+ const id = f.item?._id || f.item?.id;
1924
+ const reason = f.reason || f.item?.error?.reason || 'Unknown error';
1925
+ if (id) {
1926
+ map.set(id, reason);
1927
+ }
1928
+ return map;
1929
+ }, new Map());
1930
+ // error 정보가 있는 경우 item에 추가
1931
+ const itemsWithError = list.map(item => {
1932
+ const id = item._id ?? item.id;
1933
+ const error = id && failedMap.has(id) ? failedMap.get(id) : undefined;
1934
+ return error ? { ...item, error } : item;
1935
+ });
1936
+ results.push(...itemsWithError);
1937
+ if (!$res?.last?._id)
1938
+ break;
1939
+ last = $res?.last;
1940
+ }
1941
+ const total = results.length;
1942
+ const failed = results.filter(N => !!N.error);
1943
+ return { index: indexName, total, failed };
1944
+ }
1945
+ /** bulk items with auto-complete options */
1946
+ async bulkItems(items, options) {
1947
+ return this.elastic.bulkItems(items, options);
1948
+ }
1949
+ }
1950
+ exports.OS2Instance = OS2Instance;
1951
+ /**
1952
+ * internal tools
1953
+ */
1954
+ const $X2 = {
1955
+ /** describe endpoint-url into component */
1956
+ describeEndpointUrl: (url, options) => {
1957
+ const errScope = options?.errScope ?? `describeEndpointUrl()`;
1958
+ const throwable = options?.throwable ?? true;
1959
+ url = /^\/\/[a-z0-9]+/.test(url) ? `https:${url}` : url;
1960
+ if (throwable) {
1961
+ if (!url)
1962
+ throw new Error(`@url(string) is required - ${errScope}`);
1963
+ if (!(url?.startsWith('http://') || url?.startsWith('https://')))
1964
+ throw new Error(`@url[${url ?? ''}] is invalid (no http) - ${errScope}`);
1965
+ }
1966
+ const $url = new URL(url);
1967
+ const host = engine_1.$U.S($url.hostname || $url.host);
1968
+ const isTunnel = $url.hostname === 'localhost';
1969
+ const protocol = engine_1.$U.S($url.protocol).split(':')[0] ?? '';
1970
+ const port = engine_1.$U.N($url.port, url?.startsWith('https') ? 443 : 80);
1971
+ const hosts = host.split('.');
1972
+ const isProxy = host.endsWith('.amazonaws.com') && hosts[hosts.length - 4] == 'execute-api';
1973
+ const region = isProxy ? hosts[hosts.length - 3] : undefined;
1974
+ return {
1975
+ protocol,
1976
+ host,
1977
+ port,
1978
+ region,
1979
+ isTunnel,
1980
+ isProxy,
1981
+ };
1982
+ },
1983
+ /** load local credentials (for dev) */
1984
+ loadCredentials: async (profile) => {
1985
+ const errScope = `loadCredentials(${profile ?? ''})`;
1986
+ const _profile = (name) => {
1987
+ if (typeof name === 'string')
1988
+ return name ? name : null;
1989
+ const NAME = engine_1.$U.env('NAME', '');
1990
+ return NAME && NAME !== 'none' ? NAME : null;
1991
+ };
1992
+ profile = _profile(profile);
1993
+ const credentials = await (0, tools_1.asyncCredentials)(profile);
1994
+ if (!credentials?.accessKeyId)
1995
+ throw new Error(`@profile[${profile ?? ''}] is invalid - ${errScope}`);
1996
+ return credentials;
1997
+ },
1998
+ };
1999
+ /**
2000
+ * factory of `OS2Instance`
2001
+ * - reads `OS2_ENDPOINT`, `OS2_INDEX`, `MY_DYNAMO_TABLE`, `OS2_AUTOCOMPLETE_FIELDS` (no version, no doc-type).
2002
+ */
2003
+ const _OS2 = (options) => {
2004
+ return new (class extends OS2Instance {
2005
+ // 0. load from env configuration, then 1. build the instance.
2006
+ constructor() {
2007
+ const endpoint = options?.endpoint ?? engine_1.$U.env('OS2_ENDPOINT', '');
2008
+ const indexName = options?.indexName ?? engine_1.$U.env('OS2_INDEX', 'test-x1');
2009
+ const tableName = engine_1.$U.env('MY_DYNAMO_TABLE', '');
2010
+ const autocompleteFields = options?.autocompleteFields ?? commons_1.$T.SS(engine_1.$U.env('OS2_AUTOCOMPLETE_FIELDS', ''));
2011
+ const roleName = options?.roleName;
2012
+ const userName = options?.userName;
2013
+ super({
2014
+ endpoint,
2015
+ indexName,
2016
+ tableName,
2017
+ autocompleteFields,
2018
+ roleName,
2019
+ userName,
2020
+ });
2021
+ }
2022
+ /**
2023
+ * expose the internal helpers. (ONLY for debugging)
2024
+ */
2025
+ get $X2() {
2026
+ return $X2;
2027
+ }
2028
+ })();
2029
+ };
2030
+ exports._OS2 = _OS2;
2031
+ /**
2032
+ * the default `OS2Instance`, built lazily by `_OS2()` on first access (S-25).
2033
+ * - nothing is built at import: no client, no dynamo-stream listener.
2034
+ * - without `OS2_ENDPOINT` it is an `OS2Instance` with `options === null`.
2035
+ */
2036
+ //* the factory prints one `PERF > instance $OS2` line (D40; the namespace is built at call time).
2037
+ exports.$OS2 = (0, lazy_1.lazyInstance)(() => (0, perf_1.measure)(engine_1._inf, engine_1.$U.NS('PERF', 'cyan'), 'instance $OS2', () => (0, exports._OS2)()));
2038
+ /**
2039
+ * explicit alias of `$OS2` (the same object). The name is kept; the meaning is OS2.
2040
+ */
2041
+ exports.$ES6Routed = exports.$OS2;
2042
+ //# sourceMappingURL=abstract-services.js.map