lemon-engine
Version:
Lemon Engine Module to Synchronize Node over DynamoDB + ElastiCache + Elasticsearch by [lemoncloud](https://lemoncloud.io)
1,062 lines • 131 kB
JavaScript
"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