UNPKG

lemon-core

Version:
1,212 lines 50.6 kB
"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.createHttpSearchProxy(endpoint, { credentials });
        return new Elastic6Proxy(params, proxy);
    }
    //* make a default instance.
    return new Elastic6Instance(params);
};
exports._ES6 = _ES6;
/**
 * const `$ES6`
 * - default instance as a singleton by env configuration.
 */
exports.$ES6 = (0, exports._ES6)();
//# sourceMappingURL=abstract-service.js.map