UNPKG

lemon-engine

Version:

Lemon Engine Module to Synchronize Node over DynamoDB + ElastiCache + Elasticsearch by [lemoncloud](https://lemoncloud.io)

1,062 lines 131 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var dynamodb_value_1 = __importDefault(require("./dynamodb-value")); // DynamoDB Data Converter. var crypto_1 = __importDefault(require("crypto")); //! to avoid Deprecated warning. var notify_service_1 = __importDefault(require("../plugins/notify-service")); var buildModel = function (_$, name, options) { var NS_NAME = name || 'LEM'; var $U = _$.U; // re-use global instance (utils). var $_ = _$._; // re-use global instance (_ lodash). var $MS = _$('MS'); // re-use global instance (mysql-service). var $DS = _$('DS'); // re-use global instance (dynamo-service). var $RS = _$('RS'); // re-use global instance (redis-service). var $ES5 = _$('ES'); // re-use global instance (elasticsearch-service). var $ES6 = _$('ES6'); // re-use global instance (elastic6-service). if (!$U) throw new Error('$U(utilities) is required!'); if (!$_) throw new Error('$_(underscore) is required!'); if (!$MS) throw new Error('$MS is required!'); if (!$DS) throw new Error('$DS is required!'); if (!$RS) throw new Error('$RS is required!'); // if (!$ES5) throw new Error('$ES is required!'); if (!$ES6) throw new Error('$ES6 is required!'); //! load common(log) functions var _log = _$.log; var _inf = _$.inf; var _err = _$.err; //! NAMESPACE var NS = $U.NS(NS_NAME, "green"); // NAMESPACE TO BE PRINTED. /** **************************************************************************************************************** * Public Common Interface Exported. ** ****************************************************************************************************************/ //! prepare instance. var ERR_NOT_IMPLEMENTED = function (id) { throw new Error("NOT_IMPLEMENTED - " + NS + ":" + JSON.stringify(id)); }; var thiz = new /** @class */ (function () { function class_1() { this.name = function () { return "model:" + name; }; this.hello = ERR_NOT_IMPLEMENTED; this.do_prepare = ERR_NOT_IMPLEMENTED; this.do_create = ERR_NOT_IMPLEMENTED; this.do_clone = ERR_NOT_IMPLEMENTED; this.do_search = ERR_NOT_IMPLEMENTED; this.do_read = ERR_NOT_IMPLEMENTED; this.do_readX = ERR_NOT_IMPLEMENTED; this.do_update = ERR_NOT_IMPLEMENTED; this.do_increment = ERR_NOT_IMPLEMENTED; this.do_delete = ERR_NOT_IMPLEMENTED; this.do_destroy = ERR_NOT_IMPLEMENTED; this.do_initialize = ERR_NOT_IMPLEMENTED; this.do_terminate = ERR_NOT_IMPLEMENTED; this.on_records = ERR_NOT_IMPLEMENTED; this.do_notify = ERR_NOT_IMPLEMENTED; this.do_subscribe = ERR_NOT_IMPLEMENTED; this.do_test_self = ERR_NOT_IMPLEMENTED; this.do_next_id = ERR_NOT_IMPLEMENTED; this.do_read_deep = ERR_NOT_IMPLEMENTED; this.do_saveES = ERR_NOT_IMPLEMENTED; this.do_cleanRedis = ERR_NOT_IMPLEMENTED; this.do_prepare_chain = ERR_NOT_IMPLEMENTED; this.do_finish_chain = ERR_NOT_IMPLEMENTED; } return class_1; }()); //! register as service only if valid name. if (!name.startsWith('_')) _$(name, thiz); /** **************************************************************************************************************** * Main Implementation. ** ****************************************************************************************************************/ var CONF_GET_VAL = function (name, defval) { return typeof options === 'object' && options[name] !== undefined ? options[name] : defval; }; var CONF_VERSION = CONF_GET_VAL('VERSION', 1); // initial version number(name 'V'). var CONF_REVISION = CONF_GET_VAL('REVISION', 1); // initial revision number(name 'R'). var CONF_VERSION_NAME = CONF_GET_VAL('VERSION_NAME', 'V'); // version name (Default 'V') // if null, then no version. var CONF_REVISION_NAME = CONF_GET_VAL('REVISION_NAME', 'R'); // revision name (Default 'R') var CONF_ID_INPUT = CONF_GET_VAL('ID_INPUT', 'id'); // default ID Name. (for input parameter) var CONF_ID_NAME = CONF_GET_VAL('ID_NAME', 'id'); // ID must be Number/String Type value. (for DynamoDB Table) var CONF_ID_TYPE = CONF_GET_VAL('ID_TYPE', 'test'); // type name of sequence for next-id. var CONF_ID_NEXT = CONF_GET_VAL('ID_NEXT', 0); // start number of sequence for next-id. var CONF_DYNA_TABLE = CONF_GET_VAL('DYNA_TABLE', 'TestTable'); // DynamoDB Target Table Name. var CONF_REDIS_PKEY = CONF_GET_VAL('REDIS_PKEY', 'TPKEY'); // Redis search prefix-key name. (optional) // const CONF_FIELDS = CONF_GET_VAL('FIELDS', null); // Fields to filter. ['id','owner','mid','parent','domain','name']; var CONF_DEFAULTS = CONF_GET_VAL('DEFAULTS', null); // Default set of fields (only effective in prepare) var CONF_CLONEABLE = CONF_GET_VAL('CLONEABLE', false); // Cloneable Setting. (it requires 'parent', 'cloned' fields). var CONF_CLONED_ID = CONF_GET_VAL('CLONED_ID', 'cloned'); // default ID Name. (for input parameter) var CONF_PARENT_ID = CONF_GET_VAL('PARENT_ID', 'parent'); // default ID Name. (for input parameter) var CONF_PARENT_IMUT = CONF_GET_VAL('PARENT_IMUT', true); // parent-id is imutable? //! CONF_ES : INDEX/TYPE required if to support search. master if FIELDS is null, or slave if FIELDS not empty. var CONF_ES_INDEX = CONF_GET_VAL('ES_INDEX', 'test-v1'); // ElasticSearch Index Name. (optional) var CONF_ES_TIMESERIES = CONF_GET_VAL('ES_TIMESERIES', false); // ES Timestamp for Time-Series Data (added @181120) var CONF_ES_TYPE = CONF_GET_VAL('ES_TYPE', ''); // ElasticSearch Type Name of this Table. (optional) #이게 ES6가면서 type이 의미 없어짐!. var CONF_ES_MASTER = CONF_GET_VAL('ES_MASTER', CONF_ES_TIMESERIES ? 1 : 0); // ES is master role? (default true if CONF_ES_FIELDS is null). (요건 main 노드만 있고, 일부 필드만 ES에 넣을 경우) var CONF_ES_VERSION = CONF_GET_VAL('ES_VERSION', 5); // ES Version Number. (5 means backward compartible) // _log(NS, '! CONF_ES_TIMESERIES=', CONF_ES_TIMESERIES); //! Security Configurations. var CONF_XECURE_KEY = CONF_GET_VAL('XECURE_KEY', null); // Encryption/Decryption Key. //! Initial CONF_FIELDS, CONF_FIELDS, CONF_XEC_FIELDS. var _a = (function () { var CONF_FIELDS = CONF_GET_VAL('FIELDS', null); var CONF_ES_FIELDS = CONF_GET_VAL('ES_FIELDS', CONF_ES_TIMESERIES ? null : ['updated_at', 'name']); var CONF_XEC_FIELDS = CONF_GET_VAL('XEC_FIELDS', null); // _log(NS, '! CONF_ES_FIELDS=', CONF_ES_FIELDS); var asArray = function ($conf) { return $conf && typeof $conf == 'string' ? $conf.split(',').reduce(function (L, val) { val = val.trim(); if (val) L.push(val); return val; }, []) : $conf; }; //! Validate configuration. if (CONF_FIELDS) { CONF_FIELDS = asArray(CONF_FIELDS); CONF_ES_FIELDS = asArray(CONF_ES_FIELDS || []); CONF_XEC_FIELDS = asArray(CONF_XEC_FIELDS || []); if (!Array.isArray(CONF_FIELDS)) throw new Error('FIELDS must be array!'); if (!Array.isArray(CONF_ES_FIELDS)) throw new Error('ES_FIELDS must be array!'); if (!Array.isArray(CONF_XEC_FIELDS)) throw new Error('XEC_FIELDS must be array!'); //! extract special fields like xecured. ex: '*pass' is xecured-fields. CONF_FIELDS = CONF_FIELDS.reduce(function (L, field) { var xecured = field.startsWith('*'); field = xecured ? field.substring(1) : field; if (!field) throw new Error('Invalid field name'); if (xecured && CONF_XEC_FIELDS.indexOf(field) < 0) CONF_XEC_FIELDS.push(field); L.push(field); return L; }, []); CONF_ES_FIELDS = CONF_ES_FIELDS.reduce(function (L, field) { var xecured = field.startsWith('*'); field = xecured ? field.substring(1) : field; if (!field) throw new Error('Invalid field name'); if (xecured && CONF_XEC_FIELDS.indexOf(field) < 0) CONF_XEC_FIELDS.push(field); L.push(field); return L; }, []); CONF_XEC_FIELDS.length && _inf(NS, 'XECURED-FIELDS =', CONF_XEC_FIELDS); } //! clear if CONF_FIELDS is empty. var isEmpty = !CONF_FIELDS || !CONF_FIELDS.length; if (isEmpty) { CONF_FIELDS = null; CONF_ES_FIELDS = null; CONF_XEC_FIELDS = null; } else if (CONF_ES_FIELDS && !CONF_ES_FIELDS.length) { CONF_ES_FIELDS = CONF_ES_TIMESERIES ? CONF_FIELDS : null; } //! returns finally. return [CONF_FIELDS, CONF_ES_FIELDS, CONF_XEC_FIELDS]; })(), CONF_FIELDS = _a[0], CONF_ES_FIELDS = _a[1], CONF_XEC_FIELDS = _a[2]; _log(NS, '! CONF_ES_FIELDS :=', CONF_ES_FIELDS && CONF_ES_FIELDS.join(', ')); //! VALIDATE CONFIGURATION. if (CONF_ES_TIMESERIES && CONF_REDIS_PKEY && !CONF_REDIS_PKEY.startsWith('#')) throw new Error('ES_TIMESERIES - Redis should be inactive. PKEY:' + CONF_REDIS_PKEY); if (CONF_ES_TIMESERIES && !CONF_ES_FIELDS.length) throw new Error('ES_TIMESERIES - CONF_ES_FIELDS should be valid!'); //! Notify Service var CONF_NS_NAME = CONF_GET_VAL('NS_NAME', ''); // '' means no notification services. //! ES Target Service var $ES = CONF_ES_VERSION > 5 ? $ES6 : $ES5; if (!$ES) throw new Error('$ES is required! Ver:' + CONF_ES_VERSION); //! DynamoDB Value Marshaller. var $crypto = function (passwd) { var algorithm = 'aes-256-ctr'; if (!crypto_1.default) throw new Error('crypto module is required!'); var thiz = { crypto: crypto_1.default, algorithm: algorithm, passwd: passwd }; var MAGIC = 'LM!#'; var JSON_TAG = '#JSON:'; thiz.encrypt = function (val) { val = val === undefined ? null : val; // msg = msg && typeof msg == 'object' ? JSON_TAG+JSON.stringify(msg) : msg; //! 어느 데이터 타입이든 저장하기 위해서, object로 만든다음, 암호화 시킨다. var msg = JSON.stringify({ alg: algorithm, val: val }); var buffer = new Buffer(MAGIC + (msg || ''), "utf8"); var passwd = this.passwd || ''; var cipher = crypto_1.default.createCipher(algorithm, passwd); var crypted = Buffer.concat([cipher.update(buffer), cipher.final()]); return crypted.toString(1 ? 'base64' : 'utf8'); }; thiz.decrypt = function (msg) { var buffer = new Buffer(msg || '', "base64"); var passwd = this.passwd || ''; var decipher = crypto_1.default.createDecipher(algorithm, passwd); var dec = Buffer.concat([decipher.update(buffer), decipher.final()]).toString('utf8'); if (!dec.startsWith(MAGIC)) { _err(NS, '> decrypt =', dec); throw new Error('invalid magic string. check passwd!'); } var data = dec.substr(MAGIC.length); if (data && !data.startsWith('{') && !data.endsWith('}')) { _err(NS, '> data =', data); throw new Error('invalid json string. check passwd!'); } var $msg = JSON.parse(data) || {}; // _log(NS, '! decrypt['+msg+'] =', $msg); return $msg.val; }; return thiz; }; ///////////////////////// //! Notification Service. var $NOT = notify_service_1.default(_$, '!' + CONF_NS_NAME, { NS_NAME: CONF_NS_NAME }); //! notify functions. thiz.do_notify = $NOT.do_notify; // delegate to notify-service thiz.do_subscribe = $NOT.do_subscribe; // delegate to notify-service ///////////////////////// //! Local Initialization. if (CONF_CLONEABLE && CONF_FIELDS) { if (CONF_PARENT_ID && CONF_FIELDS.indexOf(CONF_PARENT_ID) < 0) CONF_FIELDS.push(CONF_PARENT_ID); if (CONF_CLONED_ID && CONF_FIELDS.indexOf(CONF_CLONED_ID) < 0) CONF_FIELDS.push(CONF_CLONED_ID); } //! ignored parameters. var IGNORE_FIELDS = [CONF_ID_INPUT, CONF_ID_NAME, 'created_at', 'updated_at', 'deleted_at', CONF_CLONED_ID]; if (CONF_PARENT_IMUT && CONF_PARENT_ID) IGNORE_FIELDS.push(CONF_PARENT_ID); /** * 입력 파라미터 (id, node|param)로 부터 체인 실행을 위한 객체를 준비시킨다 * - prepare_chain 과 finish_chain 항상 같이 쓰임. * - 재귀적으로 호출될 수 있지만, 항상 prepare/finish 는 쌍이 맞음. * A: prepare() * .... * B: prepare() * ... * B: finish() * A: finish() * - 최초 호출자가 prepare_chain() 를 입력 파라미터에 맞게 잘 호출해야함. * - 주의 사항: 같은 ID 를 사용하는 리소스는 that 를 상호 공유할 수 있음. (ex: core+meta item) * * * ## that 객체 사용에 대한 주의 ## * - 각 attribute 는 업데이트할 항목들로, 사용자 데이터를 저장. * - '_' 으로 시작하는 필드는 내부 private attribute 로 저장에 이용 안됨. (ex: _node) * - '$' 으로 시작하는 필드는 내부 private object 로 저장에 이용 안됨. (ex: $item) * - _id : 현재 노드 핸드러에 이용할 아이디값. * - _node : 전체 원본 데이터의 값으로, 주로 캐시에 저장된 값. * - _ctx : 트랜잭션을 시작하게된, 컨텍스트가 저장됨 (API 시작시 저장해둠) * - _current_time : 현재 시각을 millisecond 값으로 정의됨. * - _current_mode : 현재 실행 그룹에서의 실행 모드. * - _method_stack[] : prepare/finish 그룹의 스택 저장. * - _is_prepared : that 객체가 이미 준비되어 있음 (이후, 초기화할 필요 없을듯) * - 대문자 : 객체를 저장하는 목적. * - meta : json 형태로 저장된 (디비에는 문자열로 저장됨). * * * @param id node-id (Number or Object) * @param $node node object (optional) * @param mode function-name (optional) * @param ctx context object to guard function resource. */ var prepare_chain = function (id, $node, mode, ctx) { id = id || 0; // make sure Zero value if otherwise. mode = mode || ''; // make sure string. // mode && _log(NS, `prepare_${mode}()... `); // _log(NS, '>> $node@1=', $node); //! determine object if 1st parameter is object. // $node = typeof id === 'object' ? id : $U.extend( // $node === undefined || (typeof $node === 'object' && !($node instanceof Promise)) // ? $node||{} : {params:$node} // , {'_id':id}); if (typeof id === 'object') { $node = !$node ? id : $U.extend(id, $node); // override with 2nd parameter if applicable. // _log(NS, '>> $node@2=', $node); } else { //! initial $node. if ($node === undefined || $node === null) { $node = {}; } else if (typeof $node === 'object' && !($node instanceof Promise)) { $node = $U.copy($node); // make copy of node. } else { $node = { params: $node }; } $node = $U.extend($node, { '_id': id }); } //! prepare object. var that = $node; // re-use $node as main object. // _log(NS, '>> that@1=', that); //! Records 이벤트 처리용 데이터 준비... if (that.records !== undefined) { return $U.promise(that); } //! Notify 관련 함수 처리. if (mode.startsWith('notify')) { return $U.promise(that); } //! if already prepared before, then just returns. if (that && that._is_prepared !== undefined && that._is_prepared) { // mode && _log(NS, `! already prepared(${mode}) `); that._current_mode = mode; // Current Running Mode. that._method_stack.push(mode); // stack up. it will be out from finish() if (that._method_stack.length > 1000) // WARN! return Promise.reject('method-stack full. size:' + that._method_stack); return $U.promise(that); } // _log(NS, '>> ID=', id); //! Check ID Type : String|Number. if (CONF_ID_TYPE.startsWith('#')) { // ID is not NUMBER id = that._id || that[CONF_ID_INPUT] || ''; } else { // ID must be Number. id = $U.N(that._id || that[CONF_ID_INPUT] || 0); } // _log(NS, '>> ID=', id); //! make sure core parameter. var curr_ms = that._current_time || $U.current_time_ms(); that._id = id; // As Number that._current_time = curr_ms; // Current Time in ms that._current_mode = mode; // Current Running Mode. that._method_stack = []; // Prepared Mode Stack. that._method_stack.push(mode); // stack up. it will be out from finish() that._updated_node = null; // Updated Node if update() that._ctx = ctx; // Context for API call. that._node = {}; // Prepare dummy clean node. that._is_prepared = true; // Mark Prepared Object. // mode && _log(NS, '> id = '+id+', current-time = '+curr_ms); // mode && _log(NS, `> prepared(${mode},${id})-that =`, $U.json(that)); //! _params_count : 입력으로 들어오는 object 의 파라마터 개수. (id 등 기본 필드는 무시) if (that._params_count === undefined) { that._params_count = 0; // const IGNORE_FIELDS = [CONF_ID_INPUT, CONF_ID_NAME, 'created_at', 'updated_at', 'deleted_at']; that = $_.reduce(that, function (that, val, key) { if (key.startsWith('_')) return that; if (key.startsWith('$')) return that; if (IGNORE_FIELDS.indexOf(key) >= 0) return that; that._params_count++; return that; }, that); } // start promise. return $U.promise(that); }; /** * Finish chain call. * - 마지막으로, 노드에 저장된 필드를 that 에 populate 시켜줌. * - that 에는 원래 읽어올 필드를 파라미터로 설정되어 있음. * - 그런데, 애초에 입력 파라미터가 없을 경우(_params_count == 0), 전체 필드를 읽어옴. * - that._auto_populate 가 있을 경우, 이 옵션에 따름. * */ var finish_chain = function (that) { if (!that) return that; //! project back _node to that by FIELDS set. var MODE = that._current_mode || ''; //! Records 이벤트 처리용 데이터 준비... if (that.records !== undefined) return $U.promise(that); //! Notify 관련 함수 처리. if (MODE.startsWith('notify')) return $U.promise(that); if (that._method_stack) that._method_stack.pop(); //! check mode if population. if (MODE !== 'read' && MODE !== 'increment' && MODE !== 'create') return that; //! auto-populate option. if (that._auto_populate !== undefined && !that._auto_populate) return that; //! yes read mode. if no params, or master var node = that._node; if (that._params_count === 0 || that._fields_count === -1) { //! copy all node into that. that = $_.reduce(node, function (that, val, key) { if (key === 'updated_at' && that[key] !== undefined) { // keep max-value. that[key] = that[key] > val ? that[key] : val; } else { that[key] = val; } return that; }, that); } else if (that._fields_count >= 0) { //! copy only the filtered fields. that = CONF_FIELDS ? CONF_FIELDS.reduce(function (that, field) { if (that[field] !== undefined && node[field] !== undefined) { that[field] = node[field]; } return that; }, that) : that; } return that; }; /** * * @param that * @returns {*} */ var my_prepare_id = function (that) { var ID = that._id; _log(NS, "- my_prepare_id(" + CONF_ID_TYPE + ", " + ID + ")...."); if (CONF_ID_TYPE.startsWith('#')) { //! 이름이 '#'으로 시작하고, CONF_ID_NEXT 가 있을 경우, 내부적 ID 생성 목적으로 시퀀스를 생성해 둔다. if (CONF_ID_TYPE && CONF_ID_TYPE.startsWith('#') && CONF_ID_TYPE.length > 1 && CONF_ID_NEXT > 0) { var ID_NAME_1 = CONF_ID_TYPE.substring(1); return $MS.do_get_next_id(ID_NAME_1) .then(function (id) { _log(NS, '> created next-id[' + ID_NAME_1 + ']=', id); that._id = id; return that; }); } // NOP } else if (CONF_ID_TYPE && !ID) { // _log(NS, '> creating next-id by type:'+CONF_ID_TYPE); return $MS.do_get_next_id(CONF_ID_TYPE) .then(function (id) { _log(NS, '> created next-id=', id); that._id = id; return that; }); } _log(NS, '> prepared-id =', ID); return Promise.resolve(that); //WARN! must return that. }; /** * CONF_FIELDS 에 정의된 항목만 노드로 활용하도록, 복사해 간다. * prepare => 파라미터 필드셋 의미 없음. (다만, 기본값 설정으로 필드 설정. 파라미터셋에 초기값 리턴) * create => 초기값으로 파라미터 필드셋. * update => 업데이트할 필드의 개수 구함. (없으면, 이후 실제 update 실행 암함.) * read => 읽어올 필드 설정해줌. (없으면, 이후 실제 read 실행 안함) * clone => 복제용 파라미터 필드셋. * * that._fields_count : that 에서 추출한 업데이트할 필드 개수. * * @param that * @returns {*} */ var _prepare_node = function (that) { if (!that._current_mode) throw new Error('._current_mode is required!'); var MODE = that._current_mode; var DEFAULTS = CONF_DEFAULTS || {}; var fields_count = 0; var node = that._node || {}; if (!CONF_FIELDS) { fields_count = -1; // all fields } else if (MODE === 'prepare' || MODE === 'create' || MODE === 'clone') { node = CONF_FIELDS.reduce(function (node, field) { if (field === CONF_ID_NAME) return node; // ignore id. if (field === CONF_VERSION_NAME) return node; // ignore version/revision number. if (field === CONF_REVISION_NAME) return node; // ignore version/revision number. //! reset fields with or without user parameter. if (MODE === 'prepare') { //- return default value via that. if (DEFAULTS[field] !== undefined) { // if defined DEFAULTS, the use it. node[field] = CONF_DEFAULTS[field]; //!WARN! DO NOT COPY BACK TO THAT UNLESS READ OPERATION. // if (that[field] !== undefined) that[field] = node[field]; // copy back to that(????) } else { if (that[field] !== undefined) node[field] = that[field]; // if not defined, use parameter. } //! reset also version/revision. if (CONF_VERSION_NAME) node[CONF_VERSION_NAME] = CONF_VERSION; if (CONF_REVISION_NAME) node[CONF_REVISION_NAME] = 0; //! copy user parameter to node. } else if (MODE === 'create') { //! reset only revision to 0. if (CONF_REVISION_NAME) node[CONF_REVISION_NAME] = CONF_REVISION - 1; // it will be increased at dynamo save. if (that[field] !== undefined) node[field] = that[field]; } else { if (that[field] !== undefined) node[field] = that[field]; } //! increment count. fields_count++; //! returns node. return node; }, node); } else if (MODE === 'update' || MODE === 'increment' || MODE === 'read') { node = CONF_FIELDS.reduce(function (node, field) { if (field === CONF_ID_NAME) return node; // ignore id. if (that[field] !== undefined) { fields_count++; } return node; }, node); } that._fields_count = fields_count; that._node = node; return that; //WARN! must return that. }; // Node State := Prepared var mark_node_prepared = function (node, current_time) { // if(!current_time) throw new Error('current_time is required!'); node.created_at = 0; node.updated_at = current_time; node.deleted_at = current_time; return node; }; // Node State := Created var mark_node_created = function (node, current_time) { // if(!current_time) throw new Error('current_time is required!'); node.created_at = current_time; node.updated_at = current_time; node.deleted_at = 0; return node; }; // Node State := Updated var mark_node_updated = function (node, current_time) { // if(!current_time) throw new Error('current_time is required!'); // node.created_at = 0; node.updated_at = current_time; // node.deleted_at = 0; return node; }; // Node State := Deleted var mark_node_deleted = function (node, current_time) { // if(!current_time) throw new Error('current_time is required!'); // node.created_at = 0; node.updated_at = current_time; node.deleted_at = current_time; return node; }; // 필요한 경우 ID 생성, 캐쉬된 노드 정보 읽어 옴. var my_prepare_node_prepared = function (that) { // if (!that._id) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); if (!that._current_time) return Promise.reject(new Error('._current_time is required!')); //! CONF_ES_TIMESERIES 일 경우, 무조건 데이터 생성으로 간주함. if (CONF_ES_TIMESERIES) { that._node = that._node || {}; return Promise.resolve(that); } //! prepare internal node. var ID = that._id; var CURRENT_TIME = that._current_time; // _log(NS, '> prepared-id =', id, ', current-time =', current_time, ', node =', $U.json(node)); //! if no id, then create new node-id. if (!ID) { that = _prepare_node(that); return my_prepare_id(that) .then(function (that) { var node = that._node || {}; that[CONF_ID_INPUT] = that._id; // make sure id input. node[CONF_ID_NAME] = that._id; // make sure id field. that._node = mark_node_prepared(node, CURRENT_TIME); return that; }); } //! read previous(old) node from dynamo. return my_read_node(that) .catch(function (e) { var msg = e && e.message || ''; if (msg.indexOf('404 NOT FOUND') < 0) throw e; _inf(NS, 'WARN! NOT FOUND. msg=', msg); return that; }) .then(function (that) { // _log(NS, '>> get-item-node old=', $U.json(that._node)); that = _prepare_node(that); //!VALIDATE [PREPARED] STATE. var node = that._node || {}; if (that._force_create) { _err(NS, 'WARN! _force_create is set!'); } else if (!node.deleted_at) { // if not deleted. _err(NS, 'INVALID STATE FOR PREPARED. ID=', ID, ', TIMES[CUD]=', [node.created_at, node.updated_at, node.deleted_at]); return Promise.reject(new Error('INVALID STATE. deleted_at:' + node.deleted_at)); } that[CONF_ID_INPUT] = that._id; // make sure id input. node[CONF_ID_NAME] = that._id; // make sure id field. that._node = mark_node_prepared(that._node, CURRENT_TIME); return that; }); }; // 신규 노드 생성 (또는 기존 노드 overwrite) 준비. var my_prepare_node_created = function (that) { if (!that._id) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); if (!that._current_time) return Promise.reject(new Error('._current_time is required!')); //! CONF_ES_TIMESERIES 일 경우, 무조건 데이터 생성으로 간주함. if (CONF_ES_TIMESERIES) { that._node = that._node || {}; return Promise.resolve(that); } //! 이전 데이터를 읽어온다. var ID = that._id; var CURRENT_TIME = that._current_time; _log(NS, '> prepared-id =', ID, ', current-time =', CURRENT_TIME); // _log(NS, '> prepared-id =', id, ', current-time =', current_time, ', node =', $U.json(node)); //! read previous(old) node from dynamo. return my_read_node(that) .then(function (that) { // _log(NS, '>> get-item-node old=', $U.json(that._node)); that = _prepare_node(that); //!VALIDATE [CREATED] STATE. var node = that._node || {}; if (that._force_create) { _inf(NS, 'WARN! _force_create is set!'); } else if (!node.deleted_at) { // if not deleted. _err(NS, 'INVALID STATE FOR CREATED. ID=', ID, ', TIMES[CUD]=', [node.created_at, node.updated_at, node.deleted_at]); return Promise.reject(new Error('INVALID STATE. deleted_at:' + node.deleted_at)); } that._node = mark_node_created(node, CURRENT_TIME); //!MARK [CREATED] return that; }); }; // 복제용 노드 (또는 기존 노드 overwrite) 준비. var my_prepare_node_cloned = function (that) { if (!that._id) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); if (!that._current_time) return Promise.reject(new Error('._current_time is required!')); //! CONF_ES_TIMESERIES 일 경우, 무조건 데이터 생성으로 간주함. if (CONF_ES_TIMESERIES) { that._node = that._node || {}; return Promise.resolve(that); } var ID = that._id; var node = that._node; var CURRENT_TIME = that._current_time; // _log(NS, '> cloned-id =', ID, ', current-time =', CURRENT_TIME, ', node =', $U.json(node)); //! read previous(old) node from dynamo. return my_read_node(that) .then(function (that) { // _log(NS, '>> get-item-node old=', $U.json(that._node)); that = _prepare_node(that); that._node = mark_node_created(that._node, CURRENT_TIME); // as created for cloning. return that; }); }; // 노드 업데이트 준비 var my_prepare_node_updated = function (that) { if (!that._id) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); if (!that._current_time) return Promise.reject(new Error('._current_time is required!')); //! CONF_ES_TIMESERIES 일 경우, 무조건 데이터 생성으로 간주함. if (CONF_ES_TIMESERIES) { that._node = that._node || {}; return Promise.resolve(that); } var ID = that._id; var node = that._node; var CURRENT_TIME = that._current_time; // _log(NS, '> updated-id =', ID, ', current-time =', CURRENT_TIME, ', node =', $U.json(node)); //! check if available fields. that = _prepare_node(that); //! ignore if no need to update due to FIELDS config. if (that._fields_count === 0) { // _log(NS, `! my_update_node() no need to update... fields_count=`+that._fields_count); return that; } //! if no node, read previous(old) node from dynamo. return my_read_node(that) .then(function (that) { // _log(NS, '>> get-item-node old=', $U.json(that._node)); that._node = mark_node_updated(that._node, CURRENT_TIME); return that; }); }; // 삭제된 노드 준비. var my_prepare_node_deleted = function (that) { if (!that._id) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); if (!that._current_time) return Promise.reject(new Error('._current_time is required!')); //! CONF_ES_TIMESERIES 일 경우, 무조건 데이터 생성으로 간주함. if (CONF_ES_TIMESERIES) { that._node = that._node || {}; return Promise.resolve(that); } var ID = that._id; // const node = that._node; var CURRENT_TIME = that._current_time; // _log(NS, '> deleted-id =', ID, ', current-time =', CURRENT_TIME, ', node =', $U.json(node)); //! if no node, read previous(old) node from dynamo. return my_read_node(that) .then(function (that) { // _log(NS, '>> get-item-node old=', $U.json(that._node)); that = _prepare_node(that); that._node = mark_node_deleted(that._node, CURRENT_TIME); return that; }); }; /** * Dynamo DB 처리 부분. * - 메인 DB 이므로, that._node 에 저장된 부분을 최종적으로 저장한다. * */ var $dynamo = { //! read do_read_dynamo: function (that) { var _a; if (!that._id) return Promise.reject(new Error('._id is required!')); // new Error() for stack-trace. var ID = that._id; _log(NS, "- dynamo: read(" + ID + ")...."); var idType = CONF_ID_TYPE.startsWith('#') ? 'String' : ''; return $DS.do_get_item(CONF_DYNA_TABLE, (_a = {}, _a[CONF_ID_NAME] = ID, _a.idType = idType, _a)) .then(function (node) { _log(NS, "> dynamo: node(" + ID + ") res=", $U.json(node)); that._node = node; return that; }); }, //! update do_update_dynamo: function (that) { var _a, _b; if (!that._id) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); // if (!that._current_time) return Promise.reject(new Error('._current_time is required!')); var ID = that._id; var node = that._node; // const current_time = that._current_time; _log(NS, "- dynamo: update(" + ID + ")...."); // _log(NS, '> node-id =', id, ', current-time =', current_time); //! copy attributes into node. var node2 = {}; var updated_count = 0; // const IGNORE_FIELDS = [CONF_ID_INPUT,CONF_ID_NAME,'created_at','updated_at','deleted_at',CONF_PARENT_ID,CONF_CLONED_ID]; var $xec = CONF_XECURE_KEY ? $crypto(CONF_XECURE_KEY) : null; for (var n in that) { if (!n) continue; if (!that.hasOwnProperty(n)) continue; n = '' + n; if (n.startsWith('_') || n.startsWith('$')) continue; if (IGNORE_FIELDS.indexOf(n) >= 0) continue; if (CONF_FIELDS && CONF_FIELDS.indexOf(n) < 0) continue; // Filtering Fields //TODO:IMPROVE - 변경된 것만 저장하면, 좀 더 개선될듯.. if (n) { node2[n] = that[n]; node[n] = that[n]; updated_count++; //! encrypt if xecured fields. if ($xec && CONF_XEC_FIELDS && CONF_XEC_FIELDS.indexOf(n) >= 0) { node[n] = that[n] ? $xec.encrypt(that[n]) : ''; node2[n] = node[n]; } } } node2.updated_at = node.updated_at; // copy time field. _log(NS, '> dynamo: updated[' + ID + '][' + updated_count + '] =', $U.json(node2)); //! save back into main. that._updated_node = null; that._updated_count = updated_count; //! if no update, then just returns. ( if (!updated_count) { if (CONF_FIELDS) return that; // ignore reject. return Promise.reject(new Error('nothing to update')); } //! Update Revision Number. R := R + 1 if (node[CONF_REVISION_NAME] !== undefined) node[CONF_REVISION_NAME] = $U.N(node[CONF_REVISION_NAME], 0) + 1; //! then, save into DynamoDB (update Revision Number. R := R + 1) return $DS.do_update_item(CONF_DYNA_TABLE, (_a = {}, _a[CONF_ID_NAME] = ID, _a), node2, CONF_REVISION_NAME ? (_b = {}, _b[CONF_REVISION_NAME] = 1, _b) : null) .then(function (_) { that._updated_node = node2; // SAVE INTO _updated. that._node = Object.assign(that._node, node2); _log(NS, "> dynamo: updated(" + ID + ") res=", $U.json(_)); return that; }); }, //! save do_save_dynamo: function (that) { var _a; if (!that._id) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); // if (!that._current_time) return Promise.reject(new Error('._current_time is required!')); var ID = that._id; var node = that._node; var MODE = that._current_mode || ''; var CURRENT_TIME = that._current_time; _log(NS, "- dynamo: save(" + ID + ")...."); //! override attributes into node. // const IGNORE_FIELDS = [CONF_ID_INPUT,CONF_ID_NAME,'created_at','updated_at','deleted_at',CONF_PARENT_ID,CONF_CLONED_ID]; if (MODE !== 'prepare') //WARN! in prepare mode, node was already populated with default value. (so do not override) { var $xec = CONF_XECURE_KEY ? $crypto(CONF_XECURE_KEY) : null; for (var key in that) { if (!key) continue; if (!that.hasOwnProperty(key)) continue; key = "" + key; if (key.startsWith('_') || key.startsWith('$')) continue; if (IGNORE_FIELDS.indexOf(key) >= 0) continue; if (CONF_FIELDS && CONF_FIELDS.indexOf(key) < 0) continue; // Filtering Fields node[key] = that[key]; //! encrypt if xecured fields. if ($xec && CONF_XEC_FIELDS && CONF_XEC_FIELDS.indexOf(key) >= 0) { node[key] = that[key] ? $xec.encrypt(that[key]) : ''; } } } //! Update Revision Number. R := R + 1 if (node[CONF_REVISION_NAME] !== undefined) node[CONF_REVISION_NAME] = $U.N(node[CONF_REVISION_NAME], 0) + 1; _log(NS, '> save@node-id =', ID, ', current-time =', CURRENT_TIME, ', node :=', $U.json(node)); //! then, save into DynamoDB return $DS.do_create_item(CONF_DYNA_TABLE, (_a = {}, _a[CONF_ID_NAME] = ID, _a), node) .then(function (_) { _log(NS, "> dynamo: saved(" + ID + ") res=", $U.json(_)); return that; }); }, //! increment do_increment_dynamo: function (that) { var _a, _b; if (!that._id) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); // if (!that._current_time) return Promise.reject(new Error('._current_time is required!')); var ID = that._id; var node = that._node; // const current_time = that._current_time; _log(NS, "- dynamo: increment(" + ID + ")...."); // _log(NS, '> node-id =', id, ', current-time =', current_time); //! copy attributes into node. var node2 = {}; var updated_count = 0; // const IGNORE_FIELDS = [CONF_ID_INPUT,CONF_ID_NAME,'created_at','updated_at','deleted_at']; for (var n in that) { if (!that.hasOwnProperty(n)) continue; n = '' + n; if (n.startsWith('_')) continue; if (n.startsWith('$')) continue; if (IGNORE_FIELDS.indexOf(n) >= 0) continue; if (CONF_FIELDS && CONF_FIELDS.indexOf(n) < 0) continue; // Filtering Fields //TODO:IMPROVE - 변경된 것만 저장하면, 좀 더 개선될듯.. if (n) { node2[n] = that[n]; node[n] = $U.N(node[n], 0) + $U.N(that[n]); updated_count++; } } node2.updated_at = node.updated_at; // copy time field. _log(NS, '> dynamo: incremented[' + ID + '] :=', $U.json(node2)); //! save back into main. that._updated_node = null; that._updated_count = updated_count; //! if no update, then just returns. ( if (!updated_count) { if (CONF_FIELDS) return that; // ignore reject. return Promise.reject(new Error('nothing to update')); } //! Update Revision Number. R := R + 1 if (node[CONF_REVISION_NAME] !== undefined) node[CONF_REVISION_NAME] = $U.N(node[CONF_REVISION_NAME], 0) + 1; //! then, save into DynamoDB (update Revision Number. R := R + 1) return $DS.do_increment_item(CONF_DYNA_TABLE, (_a = {}, _a[CONF_ID_NAME] = ID, _a), node2, CONF_REVISION_NAME ? (_b = {}, _b[CONF_REVISION_NAME] = 1, _b) : null) .then(function (_) { _log(NS, "> dynamo: increment(" + ID + ") res=", $U.json(_)); that._updated_node = node2; // SAVE INTO _updated. // that._node = Object.assign(that._node, node2); //WARN! - DO NOT ASSIGN AGAIN. ARLEADY DONE IN ABOVE. return that; }); }, //! delete do_delete_dynamo: function (that) { var _a; var ID = that._id; if (!ID) return Promise.reject(new Error('._id is required!')); _log(NS, "- dynamo: delete(" + ID + ")...."); //! do delete command. return $DS.do_delete_item(CONF_DYNA_TABLE, (_a = {}, _a[CONF_ID_NAME] = ID, _a)) .then(function (node) { _log(NS, "> dynamo: deleted(" + ID + ") res=", $U.json(node)); that._node = node || {}; return that; }); } }; /** * Redis * - Dynamo와 중간에서 메인 캐시 역활을 함. * - 상호 효과적인 동기화를 위해서, updated_at과 hash값을 활용함. * - PKEY = '#' 일 경우, $elasticsearch가 그 역활을 대신하도록 함. * * //TODO - Redis 에서 오브젝트 단위로 읽고 쓸 수 있도록 하기... */ var $redis = { /** * Read node via Redis cache. */ do_read_cache: function (that) { var ID = that._id; if (!ID) return Promise.reject(new Error('._id is required!')); //! Redis Key 가 없다면, read 실퍠로 넘겨줘야함. if (!CONF_REDIS_PKEY) return Promise.reject(that); //NOTE - '#'으로 시작하면, elasticsearch로 대신함. if (CONF_REDIS_PKEY.startsWith('#')) { //! 다만, TIMESERIES이면, DYNAMO에서 직접 읽어 온다. @181120. if (CONF_ES_TIMESERIES) { return $dynamo.do_read_dynamo(that) // STEP 2. If failed, Read Node from DynamoDB. .then(function (that) { var node = that._node || {}; if (node[CONF_ID_NAME] === undefined) return Promise.reject(new Error('404 NOT FOUND. ' + CONF_DYNA_TABLE + '.id:' + (that._id || ''))); return that; }); } //! 캐시를 대신하여, ES에서 ID 로 읽어오기. return $elasticsearch.do_read_search(that) .catch(function (err) { //! 읽은 node가 없을 경우에도 발생할 수 있으므로, Error인 경우에만 처리한다. if (err instanceof Error) { _err(NS, "! redis: read-search(" + CONF_REDIS_PKEY + ", " + ID + ") err :=", err.message || err); that._error = err; } throw that; }); } //! read via redis. _log(NS, "- redis: get-item(" + ID + ")...."); return $RS.do_get_item(CONF_REDIS_PKEY, ID).then(function (node) { // _log(NS, `> redis:get-item(${CONF_REDIS_PKEY}, ${ID}) res =`, $U.json(node)); // _log(NS, `> redis:get-item(${CONF_REDIS_PKEY}, ${ID}) res.len =`, node ? $U.json(node).length : null); if (!node) return Promise.reject(that); //WARN! reject that if not found. that._node = node; return that; }).catch(function (err) { //! 읽은 node가 없을 경우에도 발생할 수 있으므로, Error인 경우에만 처리한다. if (err instanceof Error) { _err(NS, "! redis: get-item(" + CONF_REDIS_PKEY + ", " + ID + ") err :=", err.message || err); that._error = err; } throw that; }); }, /** * Save into cache. */ do_save_cache: function (that) { var ID = that._id; if (!ID) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); if (!CONF_REDIS_PKEY) return Promise.resolve(that); //NOTE - '#'으로 시작하면, elasticsearch로 대신함. (저장 부분은, ES에서 별도로 처리해 주므로, 그냥 무시함) if (CONF_REDIS_PKEY.startsWith('#')) return Promise.resolve(that); var node = that._node; _log(NS, "- redis: create-item(" + ID + ")...."); // _log(NS, `- redis:my_save_node(${CONF_REDIS_PKEY}:${ID}). node=`, node); // _log(NS, `- redis:my_save_node(${CONF_REDIS_PKEY}:${ID}). node=`, $U.json(node)); // _log(NS, `- redis:my_save_node(${CONF_REDIS_PKEY}:${ID}). node.updated_at=`, node&&node.updated_at||0); var chain = $redis.do_set_cache_footprint(ID, node); chain = chain.then(function () { return $RS.do_create_item(CONF_REDIS_PKEY, ID, node); }) .then(function (rs) { // _log(NS, `> redis:save-item-node(${ID}) res=`, $U.json(rs)); // if(!node) return Promise.reject(that); // reject if not found. return that; }); return chain; }, /** * Update Node. */ do_update_cache: function (that) { var ID = that._id; if (!ID) return Promise.reject(new Error('._id is required!')); if (!that._node) return Promise.reject(new Error('._node is required!')); if (!CONF_REDIS_PKEY) return Promise.resolve(that); //NOTE - '#'으로 시작하면, elasticsearch로 대신함. (저장 부분은, ES에서 별도로 처리해 주므로, 그냥 무시함) if (CONF_REDIS_PKEY.startsWith('#')) return Promise.resolve(that); var node = that._node; _log(NS, "- redis: update-item(" + ID + ")...."); var chain = $redis.do_set_cache_footprint(ID, node); //WARN! IT IS NOT SUPPORTED YET!!! chain = chain.then(function () { return $RS.do_update_item(CONF_REDIS_PKEY, ID, node); }) .then(function (rs) { // _log(NS, `> redis:update-item-node(${ID}) res=`, $U.json(rs)); // if(!node) return Promise.reject(that); // reject if not found. return that; }); return chain; }, /** * Delete Node. */ do_delete_cache: function (that) { var ID = that._id; if (!ID) return Promise.reject(new Error('._id is required!')); if (!CONF_REDIS_PKEY) return Promise.resolve(that); //NOTE - '#'으로 시작하면, elasticsearch로 대신함. (저장 부분은, ES에서 별도로 처리해 주므로, 그냥 무시함) // if (CONF_REDIS_PKEY.startsWith('#')) return Promise.resolve(that) var