UNPKG

lemon-core

Version:
1,029 lines 59.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } function fulfill(value) { resume("next", value); } function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DummyElastic6Service = exports.$ERROR = exports.Elastic6Service = exports.ElasticIndexService = exports.$hangul = exports.elasticsearch = void 0; /** * `elastic6-service.ts` * - common service for elastic-search v6 * * @author Steve Jung <steve@lemoncloud.io> * @date 2019-11-20 initial version via backbone * @date 2022-02-21 optimized error handler, and search. * @date 2022-02-22 optimized w/ elastic client (elasticsearch-js) * * @copyright (C) 2019 LemonCloud Co Ltd. - All Rights Reserved. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const engine_1 = require("../../engine/"); const elasticsearch_1 = __importDefault(require("@elastic/elasticsearch")); exports.elasticsearch = elasticsearch_1.default; const hangul_service_1 = __importDefault(require("./hangul-service")); exports.$hangul = hangul_service_1.default; const tools_1 = require("../../tools"); const test_helper_1 = require("../../common/test-helper"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const NS = engine_1.$U.NS('ES6', 'green'); // NAMESPACE TO BE PRINTED. /** * convert to string. */ const _S = (v, def = '') => typeof v === 'string' ? v : v === undefined || v === null ? def : typeof v === 'object' ? engine_1.$U.json(v) : `${v}`; /** **************************************************************************************************************** * Elastic Index Service ** ****************************************************************************************************************/ /** * abstarct class: `ElasticIndexService` * - abstract class for basic Elasticsearch CRUD operations * - common operations that are shared across different versions. * TODO - support `Elastic` and `OpenSearch` */ class ElasticIndexService { /** * default constuctor w/ options. * @param options { endpoint, indexName } is required. */ constructor(options) { (0, engine_1._inf)(NS, `ElasticIndexService(${options.indexName}/${options.idName})...`); if (!options.endpoint) throw new Error('.endpoint (URL) is required'); if (!options.indexName) throw new Error('.indexName (string) is required'); // default option values: docType='_doc', idName='$id' const { client } = ElasticIndexService.instance(options.endpoint); this._options = Object.assign({ docType: '_doc', idName: '$id', version: '6.8' }, options); this._client = client; } /** * simple instance maker. * * ```js * const { client } = ElasticIndexService.instance(endpoint); * ``` * * @param endpoint service-url * @see https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/16.x/configuration.html */ static instance(endpoint) { const client = new elasticsearch_1.default.Client({ node: endpoint, ssl: { ca: process.env.elasticsearch_certificate, rejectUnauthorized: false, }, }); return { client }; } /** * get the client instance. */ get client() { return this._client; } /** * get the current options. */ get options() { return this._options; } /** * get the version from options */ get version() { const ver = engine_1.$U.F(this.options.version, 6.8); return ver; } } exports.ElasticIndexService = ElasticIndexService; /** * class: `Elastic6Service` * - extends `ElasticIndexService` and adds version-specific implementation */ class Elastic6Service extends ElasticIndexService { /** * default constuctor w/ options. * @param options { endpoint, indexName } is required. */ constructor(options) { super(options); /** * say hello of identity. */ this.hello = () => `elastic6-service:${this.options.indexName}:${this.options.version}`; (0, engine_1._inf)('Elastic6Service', `Elastic6Service(${options.indexName}/${options.idName})...`); } /** * get isOldES6 * - used when setting doctype * - used when verifying mismatched error and results of search */ get isOldES6() { return this.parsedVersion.major < 7 && this.parsedVersion.engine === 'es'; } /** * get isOldES71 * - used when verifying mismatched error */ get isOldES71() { return this.parsedVersion.major == 7 && this.parsedVersion.minor == 1 && this.parsedVersion.engine === 'es'; } /** * get isLatestOS2 * - used when verifying results of search */ get isLatestOS2() { return this.parsedVersion.major >= 2 && this.parsedVersion.engine === 'os'; } /** * get the parsedVersion */ get parsedVersion() { return this.parseVersion(this.options.version); } /** * get the root version from client * * @protected only for internal test. */ getVersion(options) { var _a, _b, _c; return __awaiter(this, void 0, void 0, function* () { const isDump = (_a = options === null || options === void 0 ? void 0 : options.dump) !== null && _a !== void 0 ? _a : false; // it consumes about >20ms const info = yield this.client.info(); const rootVersion = engine_1.$U.S(info.body.version.number); const parsedVersion = this.parseVersion(rootVersion, { throwable: true }); if (isDump) { //* save into `info.json`. const description = Object.assign({ '!': `${(_b = this.parsedVersion) === null || _b === void 0 ? void 0 : _b.engine}${this.options.version} client info` }, info); const filePath = path_1.default.resolve(__dirname, `../../../data/samples/${(_c = this.parsedVersion) === null || _c === void 0 ? void 0 : _c.engine}${this.options.version}/info.json`); yield this.saveInfoToFile(description, filePath); } return parsedVersion; }); } /** * check whether the service version matches the version provided in the options. * * @protected only for internal test. */ executeSelfTest() { return __awaiter(this, void 0, void 0, function* () { // STEP.1 read the parsed-version. const optionVersion = this.parsedVersion; // STEP.2 get the real version via `getVersion()` const rootVersion = yield this.getVersion(); // STEP.3 validate version const isEqual = optionVersion.engine === rootVersion.engine && optionVersion.major === rootVersion.major && optionVersion.minor === rootVersion.minor; // Return the comparison result return { isEqual: isEqual, optionVersion: optionVersion, rootVersion: rootVersion, }; }); } /** * parse version according to Semantic Versioning (SemVer) rules. * * @param version The version string to parse (e.g., "1.2.3", "1.2.3-alpha.1", "1.2.3+build.001"). * @param options Optional configuration for throwable behavior. * @returns A ParsedVersion object or null if parsing fails and throwable is false. */ parseVersion(version, options) { var _a; const isThrowable = (_a = options === null || options === void 0 ? void 0 : options.throwable) !== null && _a !== void 0 ? _a : true; if (!version && isThrowable) throw new Error(`@version (string) is required!`); // RegEx to match Semantic Versioning patterns const match = version === null || version === void 0 ? void 0 : version.match(/^(\d{1,2})(?:\.(\d{1,2}))?(?:\.(\d{1,2}))?(?:-([a-zA-Z0-9-.]+))?(?:\+([a-zA-Z0-9-.]+))?$/); if (!match) { if (isThrowable) throw new Error(`@version[${version}] is invalid - fail to parse`); return null; } const res = Object.assign(Object.assign({ engine: engine_1.$U.N(match[1], 10) < 6 ? 'os' : 'es', major: engine_1.$U.N(match[1], 10), minor: match[2] !== undefined ? engine_1.$U.N(match[2], 10) : 0, patch: match[3] !== undefined ? engine_1.$U.N(match[3], 10) : 0 }, (match[4] !== undefined ? { prerelease: match[4] } : {})), (match[5] !== undefined ? { build: match[5] } : {})); return res; } /** * save info to a JSON file. * @param info - The information to be saved * @param filePath - The file path where should be saved. */ saveInfoToFile(info, filePath) { return __awaiter(this, void 0, void 0, function* () { try { const directory = path_1.default.dirname(filePath); // check whether directory exists if (!fs_1.default.existsSync(directory)) { fs_1.default.mkdirSync(directory, { recursive: true }); } // write info to file fs_1.default.writeFileSync(filePath, JSON.stringify(info, null, 2)); } catch (_a) { exports.$ERROR.handler('saveIntoFile', e => { throw e; }); } }); } /** * list of index */ listIndices() { return __awaiter(this, void 0, void 0, function* () { (0, engine_1._log)(NS, `- listIndices()`); //* prepare client.. const client = this.client; const res = yield client.cat.indices({ format: 'json' }); (0, engine_1._log)(NS, `> indices =`, engine_1.$U.json(res)); // eslint-disable-next-line prettier/prettier const list0 = Array.isArray(res) ? res : (res === null || res === void 0 ? void 0 : res.body) && Array.isArray(res === null || res === void 0 ? void 0 : res.body) ? res === null || res === void 0 ? void 0 : res.body : null; if (!list0) throw new Error(`@result<${typeof res}> is invalid - ${engine_1.$U.json(res)}!`); // {"docs.count": "84", "docs.deleted": "7", "health": "green", "index": "dev-eureka-alarms-v1", "pri": "5", "pri.store.size": "234.3kb", "rep": "1", "status": "open", "store.size": "468.6kb", "uuid": "xPp-Sx86SgmhAWxT3cGAFw"} const list = list0.map(N => ({ pri: engine_1.$U.N(N['pri']), rep: engine_1.$U.N(N['rep']), docsCount: engine_1.$U.N(N['docs.count']), docsDeleted: engine_1.$U.N(N['docs.deleted']), health: _S(N['health']), index: _S(N['index']), status: _S(N['status']), uuid: _S(N['uuid']), priStoreSize: _S(N['pri.store.size']), storeSize: _S(N['store.size']), })); return { list }; }); } /** * get mapping of an index * @param indexName - name of the index */ getIndexMapping() { var _a; return __awaiter(this, void 0, void 0, function* () { const client = this.client; const indexName = this.options.indexName; const res = yield client.indices.getMapping({ index: indexName }).catch( // $ERROR.throwAsJson, exports.$ERROR.handler('getMapping', e => { const msg = (0, test_helper_1.GETERR)(e); if (msg.startsWith('404 INDEX NOT FOUND')) throw new Error(`404 NOT FOUND - index:${indexName}`); throw e; })); const mapping = (res === null || res === void 0 ? void 0 : res.body) ? (_a = res.body[indexName]) === null || _a === void 0 ? void 0 : _a.mappings : null; if (!mapping) throw new Error(`@indexName[${indexName}] is not found - ${engine_1.$U.json(res)}!`); return mapping; }); } /** * find the index by name * @param indexName - name of the index */ findIndex(indexName) { return __awaiter(this, void 0, void 0, function* () { indexName = indexName || this.options.indexName; (0, engine_1._log)(NS, `- findIndex(${indexName})`); const { list } = yield this.listIndices(); const found = list.findIndex(N => N.index == indexName); return found >= 0 ? list[found] : null; }); } /** * create index by name * @param settings - creating settings */ createIndex(settings) { return __awaiter(this, void 0, void 0, function* () { const { indexName, docType, idName, timeSeries, version } = this.options; settings = settings || Elastic6Service.prepareSettings({ docType, idName, timeSeries, version }); if (!indexName) new Error('@index is required!'); (0, engine_1._log)(NS, `- createIndex(${indexName})`); //* prepare payload const payload = Object.assign({ settings: { number_of_shards: 5, number_of_replicas: 1, } }, settings); (0, engine_1._log)(NS, `> settings[${indexName}] = `, engine_1.$U.json(payload)); //* call create index.. const client = this.client; const res = yield client.indices.create({ index: indexName, body: payload }).catch( // $ERROR.throwAsJson, exports.$ERROR.handler('create', e => { const msg = (0, test_helper_1.GETERR)(e); if (msg.startsWith('400 RESOURCE ALREADY EXISTS')) throw new Error(`400 IN USE - index:${indexName}`); throw e; })); // if (res) throw res; (0, engine_1._log)(NS, `> create[${indexName}] =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); //* build result. return { status: res.statusCode, index: indexName, acknowledged: res.body.shards_acknowledged, }; }); } /** * destroy search index */ destroyIndex() { return __awaiter(this, void 0, void 0, function* () { const { indexName } = this.options; if (!indexName) new Error('@index is required!'); (0, engine_1._log)(NS, `- destroyIndex(${indexName})`); //* call destroy index.. // const { client } = instance(endpoint); const client = this.client; const res = yield client.indices.delete({ index: indexName }).catch( // $ERROR.throwAsJson, exports.$ERROR.handler('destroy', e => { const msg = (0, test_helper_1.GETERR)(e); if (msg.startsWith('404 INDEX NOT FOUND')) throw new Error(`404 NOT FOUND - index:${indexName}`); throw e; })); // if (res) throw res; (0, engine_1._log)(NS, `> destroy[${indexName}] =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); return { status: res.statusCode, index: indexName, acknowledged: res.body.acknowledged, }; }); } /** * refresh search index - refresh index to make all items searchable */ refreshIndex() { return __awaiter(this, void 0, void 0, function* () { const { indexName } = this.options; if (!indexName) throw new Error('.indexName is required!'); (0, engine_1._log)(NS, `- refreshIndex(${indexName})`); //* call refresh index.. // const { client } = instance(endpoint); const client = this.client; const res = yield client.indices.refresh({ index: indexName }).catch( // $ERROR.throwAsJson, exports.$ERROR.handler('refresh', e => { const msg = (0, test_helper_1.GETERR)(e); if (msg.startsWith('404 INDEX NOT FOUND')) throw new Error(`404 NOT FOUND - index:${indexName}`); throw e; })); (0, engine_1._log)(NS, `> refresh[${indexName}] =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); return res.body; }); } /** * flush search index - force store changes into search index immediately */ flushIndex() { return __awaiter(this, void 0, void 0, function* () { const { indexName } = this.options; if (!indexName) throw new Error('.indexName is required!'); (0, engine_1._log)(NS, `- flushIndex(${indexName})`); //* call flush index.. // const { client } = instance(endpoint); const client = this.client; const res = yield client.indices.flush({ index: indexName }).catch( // $ERROR.throwAsJson, exports.$ERROR.handler('flush', e => { const msg = (0, test_helper_1.GETERR)(e); if (msg.startsWith('404 INDEX NOT FOUND')) throw new Error(`404 NOT FOUND - index:${indexName}`); throw e; })); (0, engine_1._log)(NS, `> flush[${indexName}] =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); return res.body; }); } /** * describe `settings` and `mappings` of index. */ describe() { return __awaiter(this, void 0, void 0, function* () { const { indexName } = this.options; //* call create index.. (0, engine_1._log)(NS, `- describe(${indexName})`); //* read settings. // const { client } = instance(endpoint); const client = this.client; const res = yield client.indices.getSettings({ index: indexName }).catch( // $ERROR.throwAsJson, exports.$ERROR.handler('describe', e => { const msg = (0, test_helper_1.GETERR)(e); if (msg.startsWith('404 INDEX NOT FOUND')) throw new Error(`404 NOT FOUND - index:${indexName}`); throw e; })); (0, engine_1._log)(NS, `> settings[${indexName}] =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); const settings = (res.body && res.body[indexName] && res.body[indexName].settings) || {}; (0, engine_1._log)(NS, `> number_of_shards =`, settings.index && settings.index.number_of_shards); // 5 (0, engine_1._log)(NS, `> number_of_replicas =`, settings.index && settings.index.number_of_replicas); // 1 //* read mappings. const res2 = yield client.indices.getMapping({ index: indexName }); (0, engine_1._log)(NS, `> mappings[${indexName}] =`, engine_1.$U.json(res2)); const mappings = (res2.body && res2.body[indexName] && res2.body[indexName].mappings) || {}; //* returns return { settings, mappings }; }); } /** * save single item * * @param id - id * @param item - item to save * @param type - document type (default: doc-type given at construction time) */ saveItem(id, item, type) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { const { indexName, docType, idName } = this.options; (0, engine_1._log)(NS, `- saveItem(${id})`); // const { client } = instance(endpoint); const client = this.client; // prepare item body and autocomplete fields const body = Object.assign(Object.assign({}, item), { [idName]: id }); const body2 = this.popullateAutocompleteFields(body); type = `${type || docType}`; const params = { index: indexName, id, body: body2 }; // check version to include 'type' in params if (this.isOldES6) { params.type = type; } if (idName === '_id') delete params.body[idName]; //WARN! `_id` is reserved in ES6. (0, engine_1._log)(NS, `> params[${id}] =`, engine_1.$U.json(params)); //NOTE - use npm `elasticsearch#13.2.0` for avoiding error. const res = yield client.create(params).catch( // $ERROR.throwAsJson, exports.$ERROR.handler('save', e => { const msg = (0, test_helper_1.GETERR)(e); //* try to overwrite document.. if (msg.startsWith('409 VERSION CONFLICT ENGINE')) { // delete body2[idName]; // do set id while update // return this.updateItem(id, body2); const param2 = { index: indexName, id, body: Object.assign({}, body2) }; if (this.isOldES6) param2.type = type; return client.index(param2); } throw e; })); (0, engine_1._log)(NS, `> create[${id}].res =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); const _version = engine_1.$U.N((_a = res.body) === null || _a === void 0 ? void 0 : _a._version, 0); const _id = (_b = res.body) === null || _b === void 0 ? void 0 : _b._id; const res2 = Object.assign(Object.assign({}, body), { _id, _version }); return res2; }); } /** * push item for time-series data. * * @param item - item to push * @param type - document type (default: doc-type given at construction time) */ pushItem(item, type) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { const { indexName, docType } = this.options; const id = ''; type = `${type || docType}`; const body = Object.assign({}, item); const body2 = this.popullateAutocompleteFields(body); (0, engine_1._log)(NS, `- pushItem(${id})`); const params = { index: indexName, type, body: body2 }; (0, engine_1._log)(NS, `> params[${id}] =`, engine_1.$U.json(params)); //NOTE - use npm `elasticsearch#13.2.0` for avoiding error. // const { client } = instance(endpoint); const client = this.client; const res = yield client.index(params).catch( // $ERROR.throwAsJson, exports.$ERROR.handler('index', e => { (0, engine_1._err)(NS, `> index[${indexName}].err =`, e instanceof Error ? e : engine_1.$U.json(e)); throw e; })); // {"_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} (0, engine_1._log)(NS, `> create[${id}].res =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); const _id = (_a = res.body) === null || _a === void 0 ? void 0 : _a._id; const _version = (_b = res.body) === null || _b === void 0 ? void 0 : _b._version; const res2 = Object.assign(Object.assign({}, body), { _id, _version }); return res2; }); } /** * read item with projections * * @param id - item-id * @param views - projections */ readItem(id, views) { var _a, _b, _c; return __awaiter(this, void 0, void 0, function* () { const { indexName, docType } = this.options; const type = `${docType}`; (0, engine_1._log)(NS, `- readItem(${id})`); const params = { index: indexName, type, id }; if (views) { const fields = []; const keys = Array.isArray(views) ? views : Object.keys(views); keys.forEach((k) => { fields.push(k); }); params._source = fields; } (0, engine_1._log)(NS, `> params[${id}] =`, engine_1.$U.json(params)); // const { client } = instance(endpoint); const client = this.client; const res = yield client.get(params).catch( // $ERROR.throwAsJson, exports.$ERROR.handler('read', e => { const msg = (0, test_helper_1.GETERR)(e); if (msg.startsWith('404 NOT FOUND')) throw new Error(`404 NOT FOUND - id:${id}`); if (msg.startsWith('404 INDEX NOT FOUND')) throw new Error(`404 NOT FOUND - index:${indexName}`); throw e; })); (0, engine_1._log)(NS, `> read[${id}].res =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); const _id = (_a = res.body) === null || _a === void 0 ? void 0 : _a._id; const _version = (_b = res.body) === null || _b === void 0 ? void 0 : _b._version; const data = (res === null || res === void 0 ? void 0 : res._source) || ((_c = res.body) === null || _c === void 0 ? void 0 : _c._source) || {}; // delete internal (analyzed) field delete data[Elastic6Service.DECOMPOSED_FIELD]; delete data[Elastic6Service.QWERTY_FIELD]; const res2 = Object.assign(Object.assign({}, data), { _id, _version }); return res2; }); } /** * delete item with projections * * @param id - item-id */ deleteItem(id) { var _a, _b, _c, _d; return __awaiter(this, void 0, void 0, function* () { const { indexName, docType } = this.options; const type = `${docType}`; (0, engine_1._log)(NS, `- readItem(${id})`); const params = { index: indexName, type, id }; (0, engine_1._log)(NS, `> params[${id}] =`, engine_1.$U.json(params)); // const { client } = instance(endpoint); const client = this.client; const res = yield client.delete(params).catch(exports.$ERROR.handler('read', e => { const msg = (0, test_helper_1.GETERR)(e); if (msg.startsWith('404 NOT FOUND')) throw new Error(`404 NOT FOUND - id:${id}`); if (msg.startsWith('404 INDEX NOT FOUND')) throw new Error(`404 NOT FOUND - index:${indexName}`); throw e; })); // {"_index":"test-v3","_type":"_doc","_id":"aaa","_version":3,"result":"deleted","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":4,"_primary_term":1} (0, engine_1._log)(NS, `> delete[${id}].res =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); const _id = (_a = res.body) === null || _a === void 0 ? void 0 : _a._id; const _version = (_b = res.body) === null || _b === void 0 ? void 0 : _b._version; const data = ((_c = res.body) === null || _c === void 0 ? void 0 : _c._source) || ((_d = res.body) === null || _d === void 0 ? void 0 : _d._source) || {}; const res2 = Object.assign(Object.assign({}, data), { _id, _version }); return res2; }); } /** * update item (throw if not exist) * `update table set a=1, b=b+2 where id='a1'` * 0. no of `a1` -> 1,2 (created) * 1. a,b := 10,20 -> 11,22 * 2. a,b := 10,null -> 11,2 (upsert) * 3. a,b := null,20 -> 1,22 * * @param id - item-id * @param item - item to update * @param increments - item to increase * @param options - (optional) request option of client. */ updateItem(id, item, increments, options) { return __awaiter(this, void 0, void 0, function* () { const { indexName, docType, idName } = this.options; const type = `${docType}`; (0, engine_1._log)(NS, `- updateItem(${id})`); item = !item && increments ? undefined : item; //* prepare params. const params = { index: indexName, id, body: {} }; // check version to include 'type' in params if (this.isOldES6) { params.type = type; } const scripts = []; if (increments) { //* it will create if not exists. params.body.upsert = Object.assign(Object.assign({}, increments), { [idName]: id }); Object.entries(increments).forEach(([key, val]) => { if (Array.isArray(val)) { // If the value is an array, append it to the existing array in the source scripts.push(`if (ctx._source['${key}'] != null && ctx._source['${key}'] instanceof List) { ctx._source['${key}'].addAll(params.increments['${key}']); } else { ctx._source['${key}'] = params.increments['${key}']; }`); } else { // If the value is a number, increment the existing field scripts.push(`if (ctx._source['${key}'] != null) { ctx._source['${key}'] += params.increments['${key}']; } else { ctx._source['${key}'] = params.increments['${key}']; }`); } }); } if (item) { // Handle item updates in the script Object.entries(item).forEach(([key]) => { scripts.push(`ctx._source['${key}'] = params.item['${key}'];`); }); } params.body.script = { source: scripts.join(' '), lang: 'painless', params: { item, increments }, }; (0, engine_1._log)(NS, `> params[${id}] =`, engine_1.$U.json(params)); // const { client } = instance(endpoint); const client = this.client; const res = yield client.update(params, options).catch(exports.$ERROR.handler('update', (e, E) => { const msg = (0, test_helper_1.GETERR)(e); //* id 아이템이 없을 경우 발생함. if (msg.startsWith('404 DOCUMENT MISSING')) throw new Error(`404 NOT FOUND - id:${id}`); //* 해당 속성이 없을때 업데이트 하려면 생길 수 있음. if (msg.startsWith('400 REMOTE TRANSPORT')) throw new Error(`400 INVALID FIELD - id:${id}`); if (msg.startsWith('404 NOT FOUND')) throw new Error(`404 NOT FOUND - id:${id}`); if (msg.startsWith('400 ACTION REQUEST VALIDATION')) throw e; if (msg.startsWith('400 INVALID FIELD')) throw e; // at ES6.8 if (msg.startsWith('400 MAPPER PARSING')) throw e; if (msg.startsWith('400 ILLEGAL ARGUMENT - Cannot apply') || msg.startsWith('400 ILLEGAL ARGUMENT - class_cast_exception:')) throw new Error(`400 ILLEGAL ARGUMENT - failed to update due to type mismatch in item's field`); // at ES7.1 if (msg.startsWith('400 ILLEGAL ARGUMENT')) throw e; // at ES7.1 throw E; })); // {"_index":"test-v3","_type":"_doc","_id":"aaa","_version":2,"result":"updated","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":8,"_primary_term":1} // {"_index":"test-v3","_type":"_doc","_id":"aaa","_version":2,"result":"noop","_shards":{"total":0,"successful":0,"failed":0}} (0, engine_1._log)(NS, `> update[${id}].res =`, engine_1.$U.json(Object.assign(Object.assign({}, res), { meta: undefined }))); const _id = res.body._id; const _version = res.body._version; const res2 = Object.assign(Object.assign({}, item), { _id, _version }); return res2; }); } /** * run search and get the raw response. * @param body - Elasticsearch Query DSL that defines the search request (e.g., size, query, filters). * @param searchType - type of search (e.g., 'query_then_fetch', 'dfs_query_then_fetch'). */ searchRaw(body, searchType) { return __awaiter(this, void 0, void 0, function* () { if (!body) throw new Error('@body (SearchBody) is required'); const { indexName, docType } = this.options; (0, engine_1._log)(NS, `- search(${indexName}, ${searchType || ''})....`); (0, engine_1._log)(NS, `> body =`, engine_1.$U.json(body)); const tmp = docType ? docType : ''; const type = docType ? `${docType}` : undefined; const params = { index: indexName, body, searchType }; // check version to include 'type' in params if (this.isOldES6) { params.type = type; } (0, engine_1._log)(NS, `> params[${tmp}] =`, engine_1.$U.json(Object.assign(Object.assign({}, params), { body: undefined }))); // const { client } = instance(endpoint); const client = this.client; const $res = yield client.search(params).catch(exports.$ERROR.handler('search', e => { (0, engine_1._err)(NS, `> search[${indexName}].err =`, e); throw e; })); // {"took":6,"timed_out":false,"_shards":{"total":4,"successful":4,"skipped":0,"failed":0},"hits":{"total":1,"max_score":0.2876821,"hits":[{"_index":"test-v3","_type":"_doc","_id":"aaa","_score":0.2876821,"_source":{"name":"AAA","@id":"aaa","a":-3,"b":-2}}]}} // _log(NS, `> search[${id}].res =`, $U.json({ ...res, meta: undefined })); // _log(NS, `> search[${tmp}].took =`, $res.took); // _log(NS, `> search[${tmp}].hits.total =`, $res.hits?.total); // _log(NS, `> search[${tmp}].hits.max_score =`, $res.hits?.max_score); // _log(NS, `> search[${tmp}].hits.hits[0] =`, $res.hits && $U.json($res.hits.hits[0])); //* return raw results. return $res === null || $res === void 0 ? void 0 : $res.body; }); } /** * run search, and get the formatmted response. * @param body - Elasticsearch Query DSL that defines the search request (e.g., size, query, filters). * @param searchType - type of search (e.g., 'query_then_fetch', 'dfs_query_then_fetch'). * */ search(body, searchType) { var _a, _b, _c; return __awaiter(this, void 0, void 0, function* () { const size = engine_1.$U.N(body.size, 0); const response = yield this.searchRaw(body, searchType); // return w/ transformed id const hits = response.hits; if (typeof hits !== 'object') throw new Error(`.hits (object) is required - hits:${engine_1.$U.json(hits)}`); //NOTE - ES6.8 w/ OS1.1 return { total: typeof ((_a = hits.total) === null || _a === void 0 ? void 0 : _a.value) === 'number' ? (_b = hits.total) === null || _b === void 0 ? void 0 : _b.value : hits.total, list: hits.hits.map((hit) => (Object.assign(Object.assign({}, hit._source), { _id: hit._id, _score: hit._score }))), last: hits.hits.length === size && size > 0 ? (_c = hits.hits[size - 1]) === null || _c === void 0 ? void 0 : _c.sort : undefined, aggregations: response.aggregations, }; }); } /** * search all until limit (-1 means no-limit) * @param body - Elasticsearch Query DSL that defines the search request (e.g., size, query, filters). * @param params - parameters including search type, limit, and retry options. */ searchAll(body, params) { var e_1, _a; return __awaiter(this, void 0, void 0, function* () { const list = []; try { for (var _b = __asyncValues(this.generateSearchResult(body, params)), _c; _c = yield _b.next(), !_c.done;) { const chunk = _c.value; chunk.forEach((N) => list.push(N)); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b); } finally { if (e_1) throw e_1.error; } } return list; }); } /** * create async generator that yields items queried until last * * @param body - Elasticsearch Query DSL that defines the search request (e.g., size, query, filters). * @param params - parameters including search type, limit, and retry options. */ generateSearchResult(body, params) { var _a, _b, _c, _d, _e, _f, _g; return __asyncGenerator(this, arguments, function* generateSearchResult_1() { const doRetry = (_b = (_a = params === null || params === void 0 ? void 0 : params.retryOptions) === null || _a === void 0 ? void 0 : _a.do) !== null && _b !== void 0 ? _b : false; const t = (_d = (_c = params === null || params === void 0 ? void 0 : params.retryOptions) === null || _c === void 0 ? void 0 : _c.t) !== null && _d !== void 0 ? _d : 5000; const maxRetries = (_f = (_e = params === null || params === void 0 ? void 0 : params.retryOptions) === null || _e === void 0 ? void 0 : _e.maxRetries) !== null && _f !== void 0 ? _f : 3; let limit = (_g = params === null || params === void 0 ? void 0 : params.limit) !== null && _g !== void 0 ? _g : -1; let retryCount = 0; if (!body.sort) body.sort = '_doc'; do { try { const { list, last } = yield __await(this.search(body, params === null || params === void 0 ? void 0 : params.searchType)); body.search_after = last; if (list.length === 0 && !body.search_after) { break; } yield yield __await(list); } catch (e) { const msg = (0, test_helper_1.GETERR)(e); if (doRetry && msg.startsWith('429 UNKNOWN') && retryCount < maxRetries) { retryCount++; yield __await((0, test_helper_1.waited)(t)); continue; } else { throw e; } } } while (body.search_after && (limit === -1 || --limit > 0)); }); } /** * prepare default setting * - migrated from engine-v2. * * @param docType document type name * @param idName id-name * @param shards number of shards (default 4) * @param replicas number of replicas (default 1) * @param timeSeries flag of TIMESERIES (default false) */ static prepareSettings(params) { const docType = params.docType === undefined ? '_doc' : params.docType; const idName = params.idName === undefined ? '$id' : params.idName; const version = engine_1.$U.F(params.version === undefined ? '6.8' : params.version); const shards = params.shards === undefined ? 4 : params.shards; const replicas = params.replicas === undefined ? 1 : params.replicas; const timeSeries = params.timeSeries === undefined ? false : params.timeSeries; //* core config. const CONF_ES_DOCTYPE = docType; const CONF_ID_NAME = idName; const CONF_ES_TIMESERIES = !!timeSeries; const ES_MAPPINGS = { // NOTE: the order of dynamic templates are important. dynamic_templates: [ // 1. Search-as-You-Type (autocomplete search) - apply to '_decomposed.*' fields { autocomplete: { path_match: `${Elastic6Service.DECOMPOSED_FIELD}.*`, mapping: { type: 'text', analyzer: 'autocomplete_case_insensitive', search_analyzer: 'standard', }, }, }, // 2. Search-as-You-Type (Korean to Alphabet sequence in QWERTY/2벌식 keyboard) - apply to '_qwerty.*' fields { autocomplete_qwerty: { path_match: `${Elastic6Service.QWERTY_FIELD}.*`, mapping: { type: 'text', analyzer: 'autocomplete_case_sensitive', search_analyzer: 'whitespace', }, }, }, // 3. string type ID field { string_id: { match_mapping_type: 'string', match: CONF_ID_NAME, mapping: { type: 'keyword', ignore_above: 256, }, }, }, // 4. any other string fields - use Hangul analyzer and create 'keyword' sub-field { strings: { match_mapping_type: 'string', mapping: { type: 'text', analyzer: 'hangul', search_analyzer: 'hangul', fields: { // keyword sub-field // 문자열 타입에 대한 템플릿을 지정하지 않으면 기본으로 ES가 '.keyword' 서브필드를 생성하나 // 문자열 타입 템플릿 재정의 시 기본으로 생성되지 않으므로 명시적으로 선언함. keyword: { type: 'keyword', ignore_above: 256, }, }, }, }, }, ], properties: { '@version': { type: 'keyword', index: false, }, created_at: { type: 'date', format: 'strict_date_optional_time||epoch_millis', }, updated_at: { type: 'date', format: 'strict_date_optional_time||epoch_millis', }, deleted_at: { type: 'date', format: 'strict_date_optional_time||epoch_millis', }, }, }; //* default settings. const ES_SETTINGS = { settings: { number_of_shards: shards, number_of_replicas: replicas, analysis: { tokenizer: { hangul: { type: 'seunjeon_tokenizer', decompound: true, deinflect: true, index_eojeol: true, pos_tagging: false, // 품사 태깅 }, edge_30grams: { type: 'edge_ngram', min_gram: 1, max_gram: 30, token_chars: ['letter', 'digit', 'punctuation', 'symbol'], }, }, analyzer: { hangul: { type: 'custom', tokenizer: 'hangul', filter: ['lowercase'], }, autocomplete_case_insensitive: { type: 'custom', tokenizer: 'edge_30grams', filter: ['lowercase'], }, autocomplete_case_sensitive: { type: 'custom', tokenizer: 'edge_30grams', filter: version < 7 && version >= 6 ? ['standard'] : [], //* error - The [standard] token filter has been removed. }, }, }, }, //* since 7.x. no mapping for types. mappings: version < 7 && version >= 6 ? { [CONF_ES_DOCTYPE]: ES_MAPPINGS } : ES_MAPPINGS, }; //* timeseries 데이터로, 기본 timestamp 값을 넣어준다. (주의! save시 current-time 값 자동 저장) if (!!CONF_ES_TIMESERIES) { ES_SETTINGS.settings.refresh_interval = '5s'; if (version < 7 && version >= 6) { ES_SETTINGS.mappings[CONF_ES_DOCTYPE].properties['@timestamp'] = { type: 'date', doc_values: true }; ES_SETTINGS.mappings[CONF_ES_DOCTYPE].properties['ip'] = { type: 'ip' }; //* clear mappings. const CLEANS = '@version,created_at,updated_at,deleted_at'.split(','); CLEANS.map(key => delete ES_SETTINGS.mappings[CONF_ES_DOCTYPE].properties[key]); } else { ES_SETTINGS.mappings.properties['@timestamp'] = { type: 'date', doc_values: true }; ES_SETTINGS.mappings.properties['ip'] = { type: 'ip' }; //* clear mappings. const CLEANS = '@version,created_at,updated_at,deleted_at'.split(','); CLEANS.map(key => delete ES_SETTINGS.properties[key]); } } //* returns settings. return ES_SETTINGS; } /** * generate autocomplete fields into the item body to be indexed * @param body item body to be saved into ES6 index * @private */ popullateAutocompleteFields(body) { const { autocompleteFields } = this.options; const isAutoComplete = autocompleteFields && Array.isArray(autocompleteFields) && autocompleteFields.length > 0; if (!isAutoComplete) return body; return autocompleteFields.reduce((N, field) => { const value = body[field]; if (typeof value == 'string' || value) { // 한글의 경우 자모 분해 형태와 영자판 변형 형태를 제공하고, 영문의 경우 원본 텍스트만 제공한다. // 다만 사용자가 공백/하이픈을 생략하고 입력하는 경우에 대응하기 위해 공백/하이픈을 제거한 형태를 공통으로 제공한다. if (hangul_service_1.default.isHangul(value, true)) { // 자모 분해 (e.g. '레몬' -> 'ㄹㅔㅁㅗㄴ') const decomposed = hangul_service_1.def