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