lemon-core
Version:
Lemon Serverless Micro-Service Platform
1,200 lines • 50.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
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 __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.$ES6 = exports._ES6 = exports.sourceToItem = exports.Elastic6Instance = exports.AbstractElastic6Instance = exports.Elastic6Synchronizer = exports.AbstractProxy = exports.ManagerProxy = exports.CoreManager = exports.CoreService = exports.filterFields = exports.asIdentityId = void 0;
/**
* `abstract-service.ts`
* - common service design pattern to build micro-service backend.
*
* @author Tim Hong <tim@lemoncloud.io>
* @date 2021-02-23 initial version
* @author Steve <steve@lemoncloud.io>
* @date 2022-02-18 optimized search w/ ES6.8
* @date 2022-02-22 optimized w/ `lemon-core#3.0` and `@elastic/elasticsearch`
* @date 2022-02-24 use `$id` in elastic-search as `_id` in dynamo-table.
* @date 2022-03-15 optimized w/ `AbstractProxy`
* @date 2022-03-17 optimized w/ `lemon-core#3.0.2` and use `env.ES6_DOCTYPE`
* @date 2022-03-31 optimized w/ unit test spec.
* @date 2022-05-19 optimized `CacheService` w/ typed key.
*
* @origin see `lemon-accounts-api/src/service/core-service.ts`
* @copyright (C) 2021 LemonCloud Co Ltd. - All Rights Reserved.
*/
const cores_1 = __importStar(require("../cores/"));
const engine_1 = require("../engine/");
const test_helper_1 = require("../common/test-helper");
const helpers_1 = require("../helpers");
const sig_v4_1 = require("./libs/sig-v4");
const request_1 = __importDefault(require("request"));
const query_string_1 = __importDefault(require("query-string"));
const NS = engine_1.$U.NS('back', 'blue'); // NAMESPACE TO BE PRINTED.
/**
* authentication helper - get identity-id from context
* @param context the current request context.
*/
function asIdentityId(context) {
var _a;
return (_a = context === null || context === void 0 ? void 0 : context.identity) === null || _a === void 0 ? void 0 : _a.identityId;
}
exports.asIdentityId = asIdentityId;
/**
* extract field names from models
* - only fields start with lowercase, or all upper.
*/
const filterFields = (fields, base = []) => fields
.filter(field => /^[a-z]+/.test(field) || /^[A-Z_]+$/.test(field))
.reduce((L, k) => {
if (k && !L.includes(k))
L.push(k);
return L;
}, [...base]);
exports.filterFields = filterFields;
/**
* abstract class `CoreService`
* - common abstract to build user service
*
* @abstract
*/
class CoreService extends cores_1.GeneralKeyMaker {
/**
* constructor
* @param tableName target table-name (or .yml dummy file-name)
* @param ns namespace of dataset
* @param idName must be `_id` unless otherwise
*/
constructor(tableName, ns, idName) {
super(ns || engine_1.$U.env('NS', 'TT'));
/** (optional) current timestamp */
this.current = 0; // for unit-test. set the current time-stamp.
/**
* override current time
*/
this.setCurrent = (current) => (this.current = current);
this.tableName = tableName || engine_1.$U.env('MY_DYNAMO_TABLE', 'Test');
this.idName = idName || '_id';
}
/**
* get the current dynamo-options.
*/
get dynamoOptions() {
return {
tableName: this.tableName,
idName: this.idName,
};
}
/**
* create storage-service w/ fields list.
*/
makeStorageService(type, fields, filter) {
//* use proxy-storage-service for both dynamo-table and dummy-data.
const storage = new cores_1.ProxyStorageService(this, this.tableName, fields, filter, this.idName);
storage.setTimer(() => (this.current ? this.current : new Date().getTime()));
return storage.makeTypedStorageService(type);
}
}
exports.CoreService = CoreService;
/**
* class: `CoreManager`
* - shared core manager for all model
*
* @abstract
*/
class CoreManager extends cores_1.AbstractManager {
/**
* constructor
* @protected
*/
constructor(type, parent, fields, uniqueField) {
super(type, parent, fields, uniqueField);
/**
* say hello()
*/
this.hello = () => `${this.storage.hello()}`;
}
/**
* get existence of model
* @param id
*/
exists(id) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.find(id)) !== null;
});
}
/**
* find model - retrieve or null
* @param id model-id
*/
find(id) {
return __awaiter(this, void 0, void 0, function* () {
return this.retrieve(id).catch(e => {
if ((0, test_helper_1.GETERR)(e).startsWith('404 NOT FOUND'))
return null;
throw e;
});
});
}
/**
* get model by key
* @param key global id(like primary-key)
*/
findByKey(key) {
return __awaiter(this, void 0, void 0, function* () {
return this.storage.storage.read(key).catch(e => {
if ((0, test_helper_1.GETERR)(e).startsWith('404 NOT FOUND'))
return null;
throw e;
});
});
}
/**
* batch get models
* - retrieve multi models per each id
* - must be matched with idList in sequence order.
*
* @param idList list of id
* @param parrallel (optional) in parrallel size
*/
getMulti(idList, parrallel) {
return __awaiter(this, void 0, void 0, function* () {
const $map = yield this.getMulti$(idList, 'id', parrallel);
return idList.map(id => { var _a; return (_a = $map[id]) !== null && _a !== void 0 ? _a : null; }).map(N => { var _a; return (((_a = N === null || N === void 0 ? void 0 : N.error) === null || _a === void 0 ? void 0 : _a.startsWith('404 NOT FOUND')) ? null : N); });
});
}
/**
* batch get models in map by idName
*/
getMulti$(idList, idName = 'id', parrallel) {
return __awaiter(this, void 0, void 0, function* () {
// 1. find items in unique
const ids = idList.reduce((L, id) => {
if (id && !L.includes(id))
L.push(id);
return L;
}, []);
// 2. find from storage.
const list = yield (0, helpers_1.my_parrallel)(ids.map(id => ({ id })), N => this.retrieve(N.id), parrallel);
// 3. convert to map
return helpers_1.$T.asMap(list, idName);
});
}
/**
* get by unique field value
* @param uniqueValue
*/
getByUniqueField(uniqueValue) {
return __awaiter(this, void 0, void 0, function* () {
return this.$unique.findOrCreate(uniqueValue);
});
}
/**
* find model by unique field value - retrieve or null
* @param uniqueValue
*/
findByUniqueField(uniqueValue) {
return __awaiter(this, void 0, void 0, function* () {
return this.getByUniqueField(uniqueValue).catch(e => {
if ((0, test_helper_1.GETERR)(e).startsWith('404 NOT FOUND'))
return null;
throw e;
});
});
}
/**
* prepare model
* - override `AbstractManager.prepare()`
*/
prepare(id, $def, isCreate = true) {
return __awaiter(this, void 0, void 0, function* () {
const $org = yield this.find(id);
if ($org)
return $org;
if (!isCreate || $def === undefined)
throw new Error(`404 NOT FOUND - ${this.type}:${id}`);
const model = this.prepareDefault($def);
// create or update lookup
if (this.$unique)
yield this.updateLookup(id, model, $org);
// save target model
const $saved = yield this.storage.save(id, Object.assign(Object.assign({}, model), { id }));
return Object.assign(Object.assign(Object.assign({}, model), $saved), { id });
});
}
/**
* update model
* - override 'AbstractManager.insert()'
*
* @deprecated use `AbstractProxy`
*/
insert(model, initSeq) {
return __awaiter(this, void 0, void 0, function* () {
const id = helpers_1.$T.S(yield this.storage.storage.nextSeq(this.type, initSeq));
return this.save(id, model);
});
}
/**
* create or update model
* @param id model id
* @param model model data
*/
save(id, model) {
return __awaiter(this, void 0, void 0, function* () {
if (!id)
throw new Error(`@id is requird - save()`);
const $org = yield this.find(id);
if (!$org)
model = this.prepareDefault(model);
// create or update lookup
if (this.$unique)
yield this.updateLookup(id, model, $org);
// save target model
const $saved = yield this.storage.save(id, model);
return Object.assign(Object.assign(Object.assign({}, $org), $saved), { id });
});
}
/**
* update model
* - override 'AbstractManager.update()'
*/
update(id, model, $inc) {
return __awaiter(this, void 0, void 0, function* () {
if (!id)
throw new Error(`@id is requird - update()`);
const $org = yield this.retrieve(id);
// update lookup
if (this.$unique)
yield this.updateLookup(id, model, $org);
// update target model
model = this.beforeSave(model, $org);
const $updated = yield this.storage.update(id, model, $inc);
return Object.assign(Object.assign(Object.assign({}, $org), $updated), { id });
});
}
/**
* update or create model
* - override 'AbstractManager.updateOrCreate()'
*/
updateOrCreate(id, model, $inc) {
return __awaiter(this, void 0, void 0, function* () {
if (!id)
throw new Error(`@id is requird - updateOrCreate()`);
const $org = yield this.prepare(id, model);
// update lookup
if (this.$unique)
yield this.updateLookup(id, model, $org);
// update target model
model = this.beforeSave(model, $org);
const $updated = yield this.storage.update(id, model, $inc);
return Object.assign(Object.assign(Object.assign({}, $org), $updated), { id });
});
}
/**
* delete model
* - override 'AbstractManager.delete()'
*/
delete(id, destroy) {
const _super = Object.create(null, {
delete: { get: () => super.delete }
});
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (!id)
throw new Error(`@id is requird - delete()`);
// delete target model
const $org = yield _super.delete.call(this, id, destroy);
// delete lookup
const uniqueField = (_a = this.$unique) === null || _a === void 0 ? void 0 : _a.field;
const uniqueValue = uniqueField && $org[uniqueField];
if (uniqueValue)
yield this.storage.delete(this.$unique.asLookupId(uniqueValue));
return $org;
});
}
/**
* prepare default-model when creation
* @param $def base-model
*/
prepareDefault($def) {
return Object.assign({}, $def);
}
/**
* update lookup and delete old one if exists
*/
updateLookup(id, model, $org) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const uniqueField = (_a = this.$unique) === null || _a === void 0 ? void 0 : _a.field;
const newUniqueValue = uniqueField && model[uniqueField];
const oldUniqueValue = uniqueField && ($org === null || $org === void 0 ? void 0 : $org[uniqueField]);
// update lookup.
if (newUniqueValue && newUniqueValue !== oldUniqueValue) {
yield this.$unique.updateLookup(Object.assign({ id }, model));
// remove old lookup
if (oldUniqueValue) {
yield this.storage.delete(this.$unique.asLookupId(oldUniqueValue));
}
}
});
}
}
exports.CoreManager = CoreManager;
/**
* proxy of manager
* - save model internally, and update only if changed properties.
* Model extends CoreModel<ModelType>, ModelType extends string
*/
// export class ManagerProxy<T, U extends CoreManager<T, any, any>> {
class ManagerProxy {
constructor(proxy, mgr) {
/**
* store the origin model.
* - `null` means `404 not found`
*/
this._org = {};
/**
* store the updated one.
*/
this._new = {};
/**
* 객체 정규화 시킴.
* - null 에 대해서는 특별히 처리.
*/
this.normal = (N) => Object.keys(N || {}).reduce((M, k) => {
if (k.startsWith('_') || k.startsWith('$'))
return M;
const v = N[k];
//* `null` 은 DynamoDB에서 비어있는 문자임.
M[k] = v === null ? '' : v;
return M;
}, {});
/**
* override w/ model
* @param $org the origin model by `.get(id)`
* @param model the new model.
*/
this.override = ($org, model) => {
const fields = this.$mgr.FIELDS;
//* update(set) all properties.
Object.entries(model).forEach(([key, val]) => {
if (!fields || fields.includes(key)) {
$org[key] = val;
}
});
return $org;
};
this.$mgr = mgr;
proxy.register(this);
}
/**
* get storage linked.
*/
get storage() {
return this.$mgr.storage;
}
/**
* read the origin node (cloned not to change).
*/
org(id, raw = false) {
const O = this._org[id];
return O === undefined ? null : raw ? O : Object.assign({}, O);
}
/**
* check if already read.
*/
has(id) {
return this.org(id) ? true : false;
}
/**
* read the node.
* @param id object-id
* @param defaultOrThrow (optional) create if not exists, or flag to throw error
*/
get(id, defaultOrThrow) {
return __awaiter(this, void 0, void 0, function* () {
const err404 = `404 NOT FOUND - proxy/${this.$mgr.type}/id:${id}`;
const throwable = typeof defaultOrThrow === 'boolean' ? defaultOrThrow : true;
const $def = typeof defaultOrThrow === 'boolean' ? null : defaultOrThrow;
// STEP.0 validate if null (404 error)
if (this._org[id] === null && !$def) {
if (throwable)
throw new Error(err404);
return null;
}
// STEP.1 find from `new`
const N = this._new[id];
if (N !== undefined)
return N;
// OR, READ (or CREATE) from storage.
const M = !$def ? yield this.$mgr.retrieve(id).catch(test_helper_1.NUL404) : yield this.$mgr.prepare(id, $def, true);
if (M === null) {
this._org[id] = null; //* null 로 저장해두고, 다음에 호출할때 에러 발생.
if (throwable)
throw new Error(err404);
return null;
}
const M2 = this.normal(M);
this._org[id] = M2; //* 원본 저장.
// const M3 = { ...M2 }; //* 클론 생성.
const M3 = JSON.parse(engine_1.$U.json(M2)); //* deep clone.
this._new[id] = M3; //* 클론 저장.
return M3;
});
}
/**
* update the node.
*/
set(id, model) {
return __awaiter(this, void 0, void 0, function* () {
if (!model)
throw new Error(`@model (object) is required - proxy/${this.$mgr.type}/id:${id}!`);
const O = yield this.get(id);
return this.override(O, model);
});
}
/**
* increment the field of Object[id]
* !WARN! this incremented properties should NOT be updated later.
*/
inc(id, model) {
return __awaiter(this, void 0, void 0, function* () {
if (!model)
throw new Error(`@model (object) is required - proxy/${this.$mgr.type}/id:${id}!`);
const $inc = Object.entries(model).reduce((M, [k, v]) => {
if (typeof v === 'number')
M[k] = v;
return M;
}, {});
const keys = Object.keys($inc);
if (!keys.length)
throw new Error(`@model (object) is empty to inc() - proxy/${this.$mgr.type}/id:${id}!`);
//* try to increment, and update the latest to both org and new.
const $res = yield this.$mgr.storage.update(id, null, $inc);
const $new = yield this.get(id);
const $org = this.org(id, true);
return keys.reduce((N, k) => {
const key = k;
N[key] = $org[key] = $res[key];
return N;
}, $new);
});
}
/**
* get all the updated node.
*
* @param onlyUpdated flag to return the only updated set. (useful to check whether to update really!)
*/
alls(onlyUpdated = true, onlyValid = true) {
const ids = Object.keys(this._new).sort();
return ids.reduce((M, id) => {
const O = this._org[id];
const N = this._new[id];
const N2 = this.$mgr.onBeforeSave(Object.assign({}, N), O);
const N3 = onlyUpdated ? helpers_1.$T.diff(O, N2, onlyValid) : N2;
M[id] = N3;
return M;
}, {});
}
}
exports.ManagerProxy = ManagerProxy;
/**
* class: `AbstractProxy`
* - common abstract based class for Proxy
*/
class AbstractProxy {
/**
* constructor of proxy.
* @param service user service instance
* @param parrallel parrallel count (default 2)
* @param cacheScope prefix of cache-key (like `lemon:SS:` or `lemon:SS:user`)
*/
constructor(context, service, parrallel = 2, cacheScope) {
/**
* say hello().
*/
this.hello = () => `manager-proxy:${this.service.NS}/${this.service.tableName}`;
/**
* list of manager-proxy
*/
this._proxies = [];
/**
* report via slack.
*/
this.report = (title, data) => __awaiter(this, void 0, void 0, function* () {
const context = this.context;
return (0, helpers_1.$slack)(title, data, null, { context }).catch(e => `ERR:${(0, test_helper_1.GETERR)(e)}`);
});
/**
* the cached identity model
*
* @deprecated useless anymore since 3.2.10
*/
this._identity = {};
this.context = context;
this.service = service;
this.parrallel = parrallel;
// create cache
const endpoint = engine_1.$U.env('CACHE_ENDPOINT', '');
if (cacheScope && endpoint.startsWith('redis:')) {
this.cache = cores_1.CacheService.create({ type: 'redis', endpoint, ns: cacheScope });
}
}
/**
* get all proxies in list.
*/
get allProxies() {
return this._proxies;
}
/**
* register this.
*
* @return size of proxies.
*/
register(mgr) {
this._proxies.push(mgr);
return this._proxies.length;
}
/**
* save all updates by each proxies.
* - 업데이트할 항목을 모두 저장함
*
* @param options running parameters.
*/
saveAllUpdates(options) {
return __awaiter(this, void 0, void 0, function* () {
const parrallel = engine_1.$U.N(options === null || options === void 0 ? void 0 : options.parrallel, this.parrallel);
// STEP.1 prepare the list of updater.
const list = this.allProxies.reduce((L, $p) => {
const $set = $p.alls(true, options === null || options === void 0 ? void 0 : options.onlyValid);
return Object.entries($set).reduce((L, [id, N]) => {
const hasUpdate = Object.keys(N).length > 0;
if (hasUpdate) {
(0, engine_1._log)(NS, `>> ${$p.$mgr.type}/${id} =`, engine_1.$U.json(N));
const _ = () => $p.$mgr.storage.update(id, N);
L.push({ id, N, _ });
}
return L;
}, L);
}, []);
// STEP.2 finally update storage.
return (0, helpers_1.my_parrallel)(list, (N) => __awaiter(this, void 0, void 0, function* () {
return typeof N._ === 'function' ? N._() : null;
}), parrallel);
});
}
/**
* featch identity-acess from `lemon-accounts-api`
*
* @deprecated useless anymore since 3.2.10
*/
fetchIdentityAccess(identityId, domain) {
return __awaiter(this, void 0, void 0, function* () {
domain = helpers_1.$T.S(domain, this.context.domain);
if (!identityId)
throw new Error(`.identityId (string) is required - fetchAccess(${domain})`);
// 1. get user detail by invoking 'lemon-accounts-api/pack-context'
const service = '//lemon-accounts-api/oauth/0/pack-context';
const body = { domain, identityId };
const $identity = yield (0, helpers_1.$protocol)(this.context, service)
.execute({}, body, 'POST')
.catch(test_helper_1.NUL404);
(0, engine_1._log)(NS, `> identity[${domain}] =`, engine_1.$U.json($identity));
//WARN! - $identity can be null (or .Account can be null)
// if (!$identity?.Account)
// throw new Error(`.Account(NextIdentityAccess) is invalid - fetchAccess(${domain}/${identityId})`);
return { identityId, $identity };
});
}
/**
* fetch(or load) identity.
*
* @param identityId id to find
* @param force (optional) force to reload if not available
* @returns the cached identity-access
*
* @deprecated useless anymore since 3.2.10
*/
getIdentity$(identityId, force) {
return __awaiter(this, void 0, void 0, function* () {
if (!identityId)
return null;
// STEP.1 check if in stock.
const val = this._identity[identityId];
if (val !== undefined && !force)
return val;
// STEP.2 fetch remotely, and save in cache.
const { $identity } = yield this.fetchIdentityAccess(identityId);
this._identity[identityId] = $identity ? $identity : null; //* mark as 'null' not to fetch futher
return $identity;
});
}
/**
* get current identity-id
*/
getCurrentIdentityId(throwable = true) {
return __awaiter(this, void 0, void 0, function* () {
const identityId = asIdentityId(this.context) || '';
if (!identityId && throwable)
throw new Error(`400 NOT ALLOWED - getCurrentIdentity(${identityId || ''})`);
return identityId;
});
}
/**
* get the current identity object (or throw access-error)
*
* @deprecated useless anymore since 3.2.10
*/
getCurrentIdentity$(throwable = true) {
return __awaiter(this, void 0, void 0, function* () {
const identityId = yield this.getCurrentIdentityId(throwable);
if (!identityId && !throwable)
return null;
return this.getIdentity$(identityId);
});
}
}
exports.AbstractProxy = AbstractProxy;
/**
* class `Elastic6Synchronizer`
* - listen DynamoDBStream events and index into Elasticsearch
*/
class Elastic6Synchronizer {
/**
* constructor
* @param elastic elastic6-service instance
* @param dynamoOptions dynamo options
*/
constructor(elastic, dynamoOptions) {
/**
* internal callback for filtering
* @private
*/
this.filter = (id, item) => {
var _a, _b;
const handler = this.synchronizerMap.get(item.type);
if (handler)
return (_b = (_a = handler.filter) === null || _a === void 0 ? void 0 : _a.call(handler, id, item)) !== null && _b !== void 0 ? _b : true; // 핸들러를 등록했다면 filter 메서드를 정의하지 않았더라도 sync 한다.
return false; // 핸들러가 등록되지 않았다면 sync하지 않는다.
};
/**
* internal callback on before synchronization
* @private
*/
this.onBeforeSync = (id, eventName, item, diff, prev) => __awaiter(this, void 0, void 0, function* () {
var _a;
const handler = this.synchronizerMap.get(item.type);
if (handler)
yield ((_a = handler.onBeforeSync) === null || _a === void 0 ? void 0 : _a.call(handler, id, eventName, item, diff, prev));
});
/**
* internal callback on after synchronization
* @private
*/
this.onAfterSync = (id, eventName, item, diff, prev) => __awaiter(this, void 0, void 0, function* () {
var _b;
const handler = this.synchronizerMap.get(item.type);
if (handler)
yield ((_b = handler.onAfterSync) === null || _b === void 0 ? void 0 : _b.call(handler, id, eventName, item, diff, prev));
});
if (!elastic)
throw new Error(`@elastic (elastic-service) is required!`);
if (!dynamoOptions)
throw new Error(`@dynamoOptions (object) is required!`);
//* build dynamo-options as default.
const options = Object.assign(Object.assign({}, dynamoOptions), { idName: dynamoOptions.idName || '_id' });
//* create sync-handler w/ this.
const listener = cores_1.LambdaDynamoStreamHandler.createSyncToElastic6(options, elastic, this.filter.bind(this), this.onBeforeSync.bind(this), this.onAfterSync.bind(this));
cores_1.default.lambda.dynamos.addListener(listener); // register DynamoStream event listener
//* prepare default synchro
this.synchronizerMap = new Map();
this.defModelSynchronizer = new (class {
filter(id, item) {
const type = `${(item === null || item === void 0 ? void 0 : item.type) || ''}`;
const stereo = `${(item === null || item === void 0 ? void 0 : item.stereo) || ''}`;
return !(type.startsWith('#') || stereo.startsWith('#')); // special purpose item. do not index.
}
})();
}
/**
* set synchronizer for the model
* @param type the model-type
* @param handler (optional) custom synchronizer.
*/
enableSynchronization(type, handler) {
this.synchronizerMap.set(type, handler !== null && handler !== void 0 ? handler : this.defModelSynchronizer);
}
}
exports.Elastic6Synchronizer = Elastic6Synchronizer;
/**
* class `AbstractElastic6Instance`
* - to manipulate the shared Elasticsearch resources.
*/
class AbstractElastic6Instance {
/**
* default constructor
*/
constructor({ endpoint, indexName, esVersion, esDocType, tableName, autocompleteFields, }) {
// initialize Elasticsearch only if there are valid endpoint & index
if (endpoint && indexName) {
const options = {
endpoint,
indexName,
version: esVersion,
autocompleteFields,
};
if (esDocType)
options.docType = esDocType;
this.elastic = new cores_1.Elastic6Service(options);
this.client = this.elastic.client;
this.query = new cores_1.Elastic6QueryService(this.elastic);
this.synchronizer = new Elastic6Synchronizer(this.elastic, { tableName });
}
}
/**
* read the current elastic6-option.
*/
get options() {
if (!this.elastic)
return null;
return this.elastic.options;
}
/**
* create Elasticsearch index w/ custom settings
*/
createIndex() {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
if (this.elastic) {
const { docType, idName } = this.options;
const settings = cores_1.Elastic6Service.prepareSettings({ docType, idName }); // default setting
const { version } = this.elastic.options;
// force set type of 'score_' field
const ver = engine_1.$U.F(version, 7.0);
const $set = { score_: { type: 'half_float' } };
if (ver < 7.0) {
settings.mappings[docType].properties = Object.assign(Object.assign({}, (((_a = settings.mappings[docType]) === null || _a === void 0 ? void 0 : _a.properties) || {})), $set);
}
else {
settings.mappings.properties = Object.assign(Object.assign({}, (((_b = settings.mappings) === null || _b === void 0 ? void 0 : _b.properties) || {})), $set);
}
return this.elastic.createIndex(settings);
}
return null;
});
}
/**
* destroy Elasticsearch index
*/
destroyIndex() {
return __awaiter(this, void 0, void 0, function* () {
return this.elastic && (yield this.elastic.destroyIndex());
});
}
/**
* display index settings and mappings
*/
describeIndex() {
return __awaiter(this, void 0, void 0, function* () {
return this.elastic && (yield this.elastic.describe());
});
}
/**
* multi get
* @param _ids _id list
*/
mget(_ids) {
return __awaiter(this, void 0, void 0, function* () {
const $res = yield this.client.mget({
index: this.options.indexName,
type: this.options.docType,
body: {
docs: _ids.map(_id => ({ _id })),
},
});
// _log(NS, `> res =`, $U.json({ ...$res, meta: undefined }));
const { docs } = $res.body;
const idName = this.options.idName;
return docs.map((doc) => (doc.found ? sourceToItem(doc._source, idName) : null));
});
}
/**
* search raw query
* @param body Elasticsearch Query DSL
* @param params see 'search_type' in Elasticsearch documentation
*/
search(body, params) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.elastic)
throw new Error(`Could not read Elasticsearch endpoint or index setting.`);
const searchType = params === null || params === void 0 ? void 0 : params.searchType;
const elastic = (params === null || params === void 0 ? void 0 : params.indexName)
? new cores_1.Elastic6Service(Object.assign(Object.assign({}, this.options), { indexName: params.indexName }))
: this.elastic;
return elastic.search(body, searchType);
});
}
/**
* create async generator that yields items queried until last
* @param body Elasticsearch Query DSL
* @param searchType see 'search_type' in Elasticsearch documentation
*/
generateSearchResult(body, searchType) {
return __asyncGenerator(this, arguments, function* generateSearchResult_1() {
if (!body.sort)
body.sort = '_doc';
do {
const { list, last } = yield __await(this.search(body, { searchType }));
body.search_after = last;
yield yield __await(list);
} while (body.search_after);
});
}
}
exports.AbstractElastic6Instance = AbstractElastic6Instance;
/**
* class: `Elastic6Instance`
* - default agent to handle search.
*/
class Elastic6Instance extends AbstractElastic6Instance {
constructor(params) {
super(params);
}
hello() {
return `Elastic6Instance`;
}
/**
* expose the internal helpers. (ONLY for debugging)
*/
get $X() {
return $X;
}
}
exports.Elastic6Instance = Elastic6Instance;
/**
* (internal) class: `Elastic6Proxy`
* - proxied agent to handle `search()`
*/
class Elastic6Proxy extends Elastic6Instance {
constructor(params, proxy) {
super(params);
this.proxy = proxy;
}
hello() {
return `Elastic6Proxy`;
}
/**
* search raw query
*
* @override
*/
search(body, params) {
const _super = Object.create(null, {
search: { get: () => super.search }
});
var _a, _b, _c, _d, _e;
return __awaiter(this, void 0, void 0, function* () {
const _hmac = (s, delim = ':', prefix = 'v1') => [prefix, engine_1.$U.hmac(Array.isArray(s) ? s.join(delim) : s, 'base64')].join(delim);
//* use proxy if possible.
if (this.proxy) {
const service = (_a = (0, helpers_1.$info)()) === null || _a === void 0 ? void 0 : _a.service;
const indexName = (_d = (_b = params === null || params === void 0 ? void 0 : params.indexName) !== null && _b !== void 0 ? _b : (_c = this.options) === null || _c === void 0 ? void 0 : _c.indexName) !== null && _d !== void 0 ? _d : '';
const signature = (_e = params === null || params === void 0 ? void 0 : params.signature) !== null && _e !== void 0 ? _e : _hmac([_hmac(service), indexName]);
const $body = { body, service, index: indexName, signature };
return this.proxy.doProxy('POST', null, null, params, $body);
}
return _super.search.call(this, body, params);
});
}
}
/**
* from Elasticsearch document to model item
* - replace the elastic's `$id` field to `_id` of dynamo-table.
*
* @param _source from elastic-search
* @param idName (optional) global id of elastic. (default is `$id`)
*/
function sourceToItem(_source, idName = '$id') {
const item = Object.assign({}, _source);
if (idName in item) {
item._id = item[idName];
delete item[idName];
}
return item;
}
exports.sourceToItem = sourceToItem;
/**
* internal helper function.
*/
const $X = {
/**
* describe (or parse) the endpoint url
* - it detects if endpoint is the proxied search.
* - it detects if endpoint needs the tunneling.
*
* @param url
*/
describeEndpointUrl: (url, options) => {
var _a, _b, _c;
const errScope = (_a = options === null || options === void 0 ? void 0 : options.errScope) !== null && _a !== void 0 ? _a : `describeEndpointUrl()`;
const throwable = (_b = options === null || options === void 0 ? void 0 : options.throwable) !== null && _b !== void 0 ? _b : true;
url = /^\/\/[a-z0-9]+/.test(url) ? `https:${url}` : url;
if (throwable) {
if (!url)
throw new Error(`@url(string) is required - ${errScope}`);
if (!((url === null || url === void 0 ? void 0 : url.startsWith('http://')) || (url === null || url === void 0 ? void 0 : url.startsWith('https://'))))
throw new Error(`@url[${url !== null && url !== void 0 ? url : ''}] is invalid (no http) - ${errScope}`);
}
const $url = new URL(url);
const host = engine_1.$U.S($url.hostname || $url.host);
const isTunnel = $url.hostname === 'localhost';
const protocol = (_c = engine_1.$U.S($url.protocol).split(':')[0]) !== null && _c !== void 0 ? _c : '';
const port = engine_1.$U.N($url.port, (url === null || url === void 0 ? void 0 : url.startsWith('https')) ? 443 : 80);
const hosts = host.split('.');
//* use `/search/0/proxy` working with deployed enpoint (requires access-key)
const isProxy = host.endsWith('.amazonaws.com') && hosts[hosts.length - 4] == 'execute-api';
const region = isProxy ? hosts[hosts.length - 3] : undefined;
return {
/** protocol */
protocol,
/** host name */
host,
/** port number */
port,
/** region in cloud */
region,
/** flag to use tunneling w/ ssh */
isTunnel,
/** flag to use proxy */
isProxy,
};
},
/**
* create http-web-proxy agent which using endpoint as proxy server.
* - originally refer to `createHttpWebProxy()`
*
* # as cases.
* as proxy agent: GET <endpoint>/<host?>/<path?>
* as direct agent: GET <endpoint>/<id?>/<cmd?>
*
* @param endpoint service url (ex: `https://xyz.execute-api.~/dev/search/0/proxy`)
* @param options optionals parameters.
*/
createHttpSearchProxy: (endpoint, options) => {
var _a, _b, _c;
const name = (_a = options === null || options === void 0 ? void 0 : options.name) !== null && _a !== void 0 ? _a : engine_1.$U.env('NAME');
const errScope = `createHttpSearchProxy(${name !== null && name !== void 0 ? name : ''})`;
if (!endpoint)
throw new Error(`@endpoint (url) is required - ${errScope}`);
const NS = engine_1.$U.NS(`X${name}`, 'magenta'); // NAMESPACE TO BE PRINTED.
const headers = options === null || options === void 0 ? void 0 : options.headers;
const encoder = (_b = options === null || options === void 0 ? void 0 : options.encoder) !== null && _b !== void 0 ? _b : ((name, path) => path);
const relayHeaderKey = (options === null || options === void 0 ? void 0 : options.relayHeaderKey) || '';
const resultKey = (_c = options === null || options === void 0 ? void 0 : options.resultKey) !== null && _c !== void 0 ? _c : '';
// initialize AWS SigV4 Client
const _client = () => {
var _a;
if (!(options === null || options === void 0 ? void 0 : options.credentials))
return null;
const $cred = options === null || options === void 0 ? void 0 : options.credentials;
const $info = $X.describeEndpointUrl(endpoint, { throwable: false });
const config = {
accessKey: $cred === null || $cred === void 0 ? void 0 : $cred.accessKeyId,
secretKey: $cred === null || $cred === void 0 ? void 0 : $cred.secretAccessKey,
serviceName: 'execute-api',
host: $info === null || $info === void 0 ? void 0 : $info.host,
region: (_a = options === null || options === void 0 ? void 0 : options.region) !== null && _a !== void 0 ? _a : $info === null || $info === void 0 ? void 0 : $info.region,
endpoint,
};
return (0, sig_v4_1.sigV4Client)(config);
};
const sigClient = _client();
/**
* class: `ApiHttpProxy`
* - http proxy client via backbone's web.
*/
return new (class {
// eslint-disable-next-line @typescript-eslint/no-empty-function
constructor() {
this.hello = () => `http-search-proxy:${name}`;
}
doProxy(method, path1, path2, $param, $body, ctx) {
if (!method)
throw new Error(`@method is required - ${errScope}`);
(0, engine_1._log)(NS, `doProxy(${method})..`);
const _isNa = (a) => a === undefined || a === null;
(0, engine_1._log)(NS, '> endpoint =', endpoint);
_isNa(path1) && (0, engine_1._log)(NS, `> host(id) =`, typeof path1, path1);
_isNa(path2) && (0, engine_1._log)(NS, `> path(cmd) =`, typeof path2, path2);
// eslint-disable-next-line prettier/prettier
const query_string = _isNa($param) ? '' : (typeof $param == 'object' ? query_string_1.default.stringify($param) : `${$param}`);
const url = endpoint +
(_isNa(path1) ? '' : `/${encoder('host', path1)}`) +
(_isNa(path1) && _isNa(path2) ? '' : `/${encoder('path', path2)}`) +
(!query_string ? '' : '?' + query_string);
const request = request_1.default;
const options = {
method,
uri: url,
headers: Object.assign({}, headers),
body: $body === null ? undefined : $body,
json: typeof $body === 'string' ? false : true,
};
//* build signed url
if (sigClient) {
const signedRequest = sigClient.signRequest({
method,
path: (_isNa(path1) ? '' : `/${encoder('host', path1)}`) +
(_isNa(path1) && _isNa(path2) ? '' : `/${encoder('path', path2)}`),
queryParams: $param,
headers: options.headers,
body: $body,
});
options.headers = Object.assign(Object.assign({}, options.headers), signedRequest.headers);
options.uri = signedRequest.url;
}
//* relay HEADERS to `WEB-API`
if (headers) {
options.headers = Object.keys(headers).reduce((H, key) => {
const val = headers[key];
const name = `${relayHeaderKey}${key}`;
const text = `${val !== null && val !== void 0 ? val : ''}`;
H[name] = text;
return H;
}, options.headers);
}
(0, engine_1._log)(NS, ' url :=', options.method, url);
(0, engine_1._log)(NS, '*', options.method, url, options.json ? 'json' : 'plain');
(0, engine_1._log)(NS, '> options =', engine_1.$U.json(options));
//* returns promise
return new Promise((resolve, reject) => {
//* start request..
request(options, function (error, response, body) {
error && (0, engine_1._err)(NS, '>>>>> requested! err=', error);
if (error)
return reject(error instanceof Error ? error : new Error((0, test_helper_1.GETERR)(error)));
//* detect trouble.
const statusCode = response.statusCode;
const statusMessage = response.statusMessage;
//* if not in success
if (statusCode !== 200 && statusCode !== 201) {
const msg = body ? (0, test_helper_1.GETERR)(body) : `${statusMessage || ''}`;
if (statusCode === 400 || statusCode === 404) {
const title = `${(statusCode == 404 ? '' : statusMessage) || 'NOT FOUND'}`.toUpperCase();
const message = msg.startsWith('404 NOT FOUND')
? msg
: `${statusCode} ${title} - ${msg}`;
return reject(new Error(message));
}
statusMessage && (0, engine_1._log)(NS, `> statusMessage[${statusCode}] =`, statusMessage);
body && (0, engine_1._log)(NS, `> body[${statusCode}] =`, engine_1.$U.json(body));
return reject(new Error(`${statusCode} ${statusMessage || 'FAILURE'} - ${msg}`));
}
//* try to parse body.
try {
if (body && typeof body == 'string' && body.startsWith('{') && body.endsWith('}')) {
body = JSON.parse(body);
}
else if (body && typeof body == 'string' && body.startsWith('[') && body.endsWith(']')) {
body = JSON.parse(body);
}
}
catch (e) {
(0, engine_1._err)(NS, '!WARN! parse(body) =', e instanceof Error ? e : engine_1.$U.json(e));
}
//* ok! succeeded.
resolve(body);
});
}).then((res) => {
if (resultKey && res && res[resultKey] !== undefined)
return res[resultKey];
return res;
});
}
})();
},
};
/**
* (internal) factory function for `$ES6`
*
* @param options (optional) for debugging
*/
const _ES6 = (options) => {
var _a, _b, _c;
// 0. load from env configuration.
const endpoint = (_a = options === null || options === void 0 ? void 0 : options.endpoint) !== null && _a !== void 0 ? _a : engine_1.$U.env('ES6_ENDPOINT', '');
const indexName = (_b = options === null || options === void 0 ? void 0 : options.indexName) !== null && _b !== void 0 ? _b : engine_1.$U.env('ES6_INDEX', 'test-v1');
const esVersion = engine_1.$U.env('ES6_VERSION', '6.8'); //* version of elastic server (default 6.8)
const esDocType = engine_1.$U.env('ES6_DOCTYPE', ''); //* version of elastic server (default `_doc`)
const tableName = engine_1.$U.env('MY_DYNAMO_TABLE', 'Test');
const autocompleteFields = helpers_1.$T.SS(engine_1.$U.env('ES6_AUTOCOMPLETE_FIELDS', ''));
// use search-proxy by detectiong automatically.
const _isProxy = () => {
var _a;
const isProxy = (_a = engine_1.$U.env('ES6_IS_PROXY', '')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
if (isProxy == '1' || isProxy == 'y' || isProxy == 'true')
return true;
if (endpoint) {
const $res = $X.describeEndpointUrl(endpoint, { throwable: false });
if ($res === null || $res === void 0 ? void 0 : $res.isProxy)
return true;
}
return false;
};
const useProxy = (_c = options === null || options === void 0 ? void 0 : options.useProxy) !== null && _c !== void 0 ? _c : _isProxy();
// prepare constructor parameters.
const params = {
endpoint,
indexName,
esVersion,
esDocType,
tableName,
autocompleteFields,
};
//* use proxy.
if (useProxy) {
const credentials = options === null || options === void 0 ? void 0 : options.credentials;
const proxy = $X.c