lemon-core
Version:
Lemon Serverless Micro-Service Platform
1,029 lines • 59.9 kB
JavaScript
"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