lemon-core
Version:
Lemon Serverless Micro-Service Platform
913 lines • 36.5 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UniqueFieldManager = exports.ModelUtil = exports.TypedStorageService = exports.ProxyStorageService = exports.GeneralModelFilter = exports.GeneralKeyMaker = void 0;
/**
* `proxy-storage-service.js`
* - common service for `accounts`
*
*
* @author Steve Jung <steve@lemoncloud.io>
* @date 2019-12-27 initial service skeleton.
*
* @copyright (C) 2019 LemonCloud Co Ltd. - All Rights Reserved.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const engine_1 = require("../../engine/");
const lemon_model_1 = require("lemon-model");
const test_helper_1 = require("../../common/test-helper");
const storage_service_1 = require("./storage-service");
const general_api_controller_1 = require("../../controllers/general-api-controller");
const NS = engine_1.$U.NS('PSTR', 'blue'); // NAMESPACE TO BE PRINTED.
/**
* class: `GeneralKeyMaker`
* - use ':' as delimiter to join [ns, type, id]
*/
class GeneralKeyMaker {
constructor(ns = '', delimiter = ':') {
this.NS = ns;
this.DELIMITER = delimiter;
}
asKey$(type, id) {
if (!id)
throw new Error('@id (model-id) is required!');
const ns = `${this.NS || ''}`;
const _id = [ns, `${type || ''}`, id].map(_ => _.replace(/[:]/gi, '-')).join(this.DELIMITER);
const res = { ns, id, type, _id };
return res;
}
}
exports.GeneralKeyMaker = GeneralKeyMaker;
/**
* class: `GeneralModelFilter`
* - general model-filter with differential update.
* - to customize, override this class.
*/
// eslint-disable-next-line prettier/prettier
class GeneralModelFilter {
/**
* default constructor
*/
constructor(fields) {
this.FIELDS = fields;
}
/**
* parse `.meta` to json
* @param model the current model
* @param origin the origin model
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
afterRead(model, origin) {
// _log(NS, `filter.afterRead(${model._id || ''})....`);
if (!model.meta)
return model;
const meta = model.meta;
model.meta = meta && typeof meta == 'string' && meta.startsWith('{') ? JSON.parse(meta) : meta;
return model;
}
/**
* filter for before saving.
* - make sure data conversion
* - move the unknown fields to `.meta`.
*
* @param model the current model
* @param origin the origin model
*/
beforeSave(model, origin) {
// _log(NS, `filter.beforeSave(${model._id})....`);
origin = origin || {};
const FIELDS = this.FIELDS && this.FIELDS.length ? this.FIELDS : null;
//! call service.onBeforeSave().
model = this.onBeforeSave(model, origin);
//TODO - accept only primitive types of field @191228.
//NOTE! - should not update the core field in save()
delete model.lock;
delete model.next;
//! load the meta data...
const $meta = (() => {
if (model.meta !== undefined && !model.meta)
return {};
const meta = model.meta || origin.meta || {}; // 만일, 파라미터에 meta 가 있다면, 클라이언트에서 직접 처리함.
return meta && typeof meta == 'string' ? JSON.parse(meta) : meta;
})();
//! move all fields to meta which is not defined in FIELDS.
model = Object.keys(model).reduce((N, key) => {
if (key.startsWith('_') || key.startsWith('$'))
return N;
if (key == 'createdAt' || key == 'updatedAt' || key == 'deletedAt')
return N;
if (/^[A-Z][A-Za-z0-9]*$/.test(key) && !/^[A-Z_]+$/.test(key))
return N; // ABC_DE 는 상수이며 OK, 다만, AbcDe 는 내부 오브젝트 이므로 무시!!!
if (key == 'meta')
return N; // meta itself.
if (FIELDS && FIELDS.indexOf(key) < 0 && FIELDS.indexOf('*' + key) < 0) {
$meta[key] = model[key];
}
else {
N[key] = model[key];
}
return N;
}, {});
model.meta = Object.keys($meta).length ? engine_1.$U.json($meta) : '';
//! handle for meta.
if (model.meta === '')
model.meta = null;
else if (typeof origin.meta == 'string' && model.meta == origin.meta)
delete model.meta;
else if (typeof origin.meta == 'object' && model.meta == engine_1.$U.json(origin.meta))
delete model.meta;
else if (!origin.meta && !model.meta)
model.meta = origin.meta;
//! filter out only the updated fields.
const res = Object.keys(model).reduce((N, key) => {
if (key.startsWith('_') || key.startsWith('$'))
return N; // ignore.
const org = origin[key];
const val = N[key];
if (!org && val)
return N;
else if (org && !val)
return N;
else if (org && typeof org === 'object') {
const org2 = engine_1.$U.json(org);
const val2 = typeof val === 'object' ? engine_1.$U.json(val) : val;
if (org2 == val2) {
delete N[key];
}
}
else if ((val === '' || val === null) && (org === null || org === undefined)) {
//NOTE! - dynamo saves null for '' string.
delete N[key];
}
else if (val === org) {
delete N[key];
}
return N;
}, model);
//! if nothing to update, then returns null.
const keys = Object.keys(model).filter(_ => !_.startsWith('_') && !_.startsWith('$'));
if (keys.length <= 0)
return null;
//! returns the filtered node.
return res;
}
/**
* called after saving the model.
* - parse `.meta` back to json object.
*
* @param model the saved model
* @param origin the origin model.
*/
afterSave(model, origin) {
return this.afterRead(model, origin);
}
/**
* called before updating the model.
* @param model the updated model
* @param incrementals (optional) incremental fields.
*/
beforeUpdate(model, incrementals) {
return model;
}
/**
* called after updating the model.
* @param model the updated model
*/
afterUpdate(model) {
return this.afterRead(model, null);
}
/**
* override this `onBeforeSave()` in sub-class.
* @param model the current model
* @param origin (optional) the origin model
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onBeforeSave(model, origin) {
//TODO - override this function.
//! conversion data-type.
// if (model.count !== undefined) model.count = $U.N(model.count, 0);
return model;
}
}
exports.GeneralModelFilter = GeneralModelFilter;
/**
* class: `ProxyStorageService`
* - support `nextSeq()`, `doLock()`, `doRelease()`
* - proxed storage-service to wrap the parent storage-service w/ more features.
* - table is supposed to have internal-key as `_id` string.
*
* **Usage**
* ```js
* type MyType = '' | 'test';
* interface MyModel extends CoreModel<MyType>{
* name?: string;
* }
* const storage = new ProxyStorageService<MyModel, MyType>(this, 'TestTable', ['id','name']);
* const $test = storage.makeTypedStorageService('test');
* ```
*/
// eslint-disable-next-line prettier/prettier
class ProxyStorageService {
/**
* create proxed storage-service.
*
* @param service service to support `CoreKeyMakeable`
* @param storage table-name or the parent storage-service
* @param fields list of fields.
* @param filters filters of `CoreModelFilterable`
* @param idName (optional) internal partition-key (default as '_id')
*/
constructor(service, storage, fields, filters, idName) {
/**
* say hello()
*/
this.hello = () => `proxy-storage-service:${this.storage.hello()}`;
/**
* read by _id
*/
this.read = (_id) => this.storage.read(_id);
/**
* read or create by _id
*/
this.readOrCreate = (_id, model) => this.storage.readOrCreate(_id, model);
/**
* save by _id
*/
this.save = (_id, model) => this.storage.save(_id, model);
/**
* update by _id
*/
this.update = (_id, model, incrementals) => this.storage.update(_id, model, incrementals);
/**
* increment by _id
*/
this.increment = (_id, model, $update) => this.storage.increment(_id, model, $update);
/**
* delete by _id
*/
this.delete = (_id) => this.storage.delete(_id);
/**
* get key-id by type+id
*/
this.asKey = (type, id) => {
if (typeof this.service.asKey == 'function') {
return this.service.asKey(type, `${id}`);
}
const $key = this.service.asKey$(type, `${id}`);
return $key[this.idName];
};
/**
* timer to generate the current-time (msec)
*/
this.$timer = null;
this.setTimer = (timer) => {
const previous = this.$timer;
this.$timer = timer;
return previous;
};
this.getTime = () => {
if (this.$timer)
return this.$timer();
return new Date().getTime();
};
this.idName = idName === undefined ? '_id' : `${idName || ''}`;
this.service = service;
this.storage =
typeof storage == 'string' ? ProxyStorageService.makeStorageService(storage, fields, idName) : storage;
this.filters = filters || new GeneralModelFilter(fields);
}
/**
* factory function to create this `proxy-storage-service`
* @param service key-makeable
* @param table table-name
* @param fields list of fields.
* @param filters model filter.
* @param idName (optional) internal partition-key (default as '_id')
*/
static create(service, table, fields, filters, idName) {
const storage = ProxyStorageService.makeStorageService(table, fields, idName);
const res = new ProxyStorageService(service, storage, fields, filters, idName);
return res;
}
/**
* get next auto-sequence number.
*
* @param type type of seqeunce.
* @param nextInit (optional) initial next value if not exist.
* @param nextStep (optional) the incremental step to get next. (default 1)
*/
nextSeq(type, nextInit, nextStep = 1) {
return __awaiter(this, void 0, void 0, function* () {
(0, engine_1._log)(NS, `nextSeq(${type}, ${nextInit !== null && nextInit !== void 0 ? nextInit : ''})..`);
if (typeof nextStep !== 'number' || nextStep < 0)
throw new Error(` [${nextStep}] is invalid - nextSeq(${type})`);
const { createdAt, updatedAt } = this.asTime();
const _id = this.asKey(ProxyStorageService.TYPE_SEQUENCE, `${type}`);
let res = yield this.storage.increment(_id, { next: nextStep }, { updatedAt }); // it will create new row if not exists. (like upset)
if (res.next == 1) {
const $key = this.service.asKey$(ProxyStorageService.TYPE_SEQUENCE, `${type}`);
nextInit = nextInit === undefined || nextInit === null ? ProxyStorageService.AUTO_SEQUENCE : nextInit;
const $upd = { next: nextInit };
const $inc = Object.assign(Object.assign({}, $key), { createdAt, updatedAt });
res = yield this.storage.increment(_id, $upd, $inc); //! increment w/ update-set
}
return res.next;
});
}
/**
* get uuid by type.
* @param type
*/
nextUuid(type) {
return __awaiter(this, void 0, void 0, function* () {
(0, engine_1._log)(NS, `nextUuid(${type})..`);
return engine_1.$U.uuid();
});
}
/**
* get time-stamp as now.
*/
asTime(currentTime) {
currentTime = currentTime || this.getTime();
const createdAt = currentTime;
const updatedAt = currentTime;
const deletedAt = currentTime;
return { createdAt, updatedAt, deletedAt };
}
/**
* delete sequence-key.
* @param type type of seqeunce.
*/
clearSeq(type) {
return __awaiter(this, void 0, void 0, function* () {
(0, engine_1._log)(NS, `nextSeq(${type})..`);
const _id = this.asKey(ProxyStorageService.TYPE_SEQUENCE, `${type}`);
yield this.storage.delete(_id);
});
}
/**
* read model by key + id with optional auto creation.
*
* @param type model-type
* @param id node-id
* @param $create (optional) initial model if not exist. (or throw 404 error)
*/
doRead(type, id, $create) {
return __awaiter(this, void 0, void 0, function* () {
const $key = this.service.asKey$(type, id);
const _id = this.asKey(type, id);
const model = yield this.storage.read(_id).catch((e) => {
if (`${e.message}`.startsWith('404 NOT FOUND') && $create) {
const { createdAt, updatedAt } = this.asTime();
return this.storage.update(_id, Object.assign(Object.assign(Object.assign({}, $create), $key), { createdAt, updatedAt, deletedAt: 0 }));
}
throw e;
});
//! make sure it has `_id`
model[this.idName] = _id;
const res = this.filters.afterRead(model);
return res;
});
}
/**
* delete model by id.
*
* @param type model-type
* @param id node-id
* @param destroy flag to destroy (real delete)
*/
doDelete(type, id, destroy = true) {
return __awaiter(this, void 0, void 0, function* () {
const _id = this.asKey(type, id);
if (destroy === undefined || destroy === true)
return this.storage.delete(_id);
const { createdAt, updatedAt, deletedAt } = this.asTime();
const $up = { updatedAt, deletedAt };
const $org = yield this.read(_id); //! it will make 404 if not found.
if (!$org.createdAt)
$up.createdAt = createdAt;
return this.update(_id, $up);
});
}
/**
* update model (or it will create automatically)
*
* @param type model-type
* @param id node-id
* @param node model
* @param incrementals (optional) fields to increment
*/
doUpdate(type, id, node, incrementals) {
return __awaiter(this, void 0, void 0, function* () {
const $inc = Object.assign({}, incrementals); //! make copy.
const _id = this.asKey(type, id);
// const $key = this.service.asKey$(type, id);
const node2 = this.filters.beforeUpdate(Object.assign(Object.assign({}, node), { [this.idName]: _id }), $inc);
delete node2['_id'];
const { updatedAt } = this.asTime();
const model = yield this.update(_id, Object.assign(Object.assign({}, node2), { updatedAt }), $inc);
//! make sure it has `_id`
model[this.idName] = _id;
return this.filters.afterUpdate(model);
});
}
/**
* update model (or it will create automatically)
*
* @param type model-type
* @param id node-id
*/
doIncrement(type, id, $inc, $up) {
return __awaiter(this, void 0, void 0, function* () {
const _id = this.asKey(type, id);
const { updatedAt } = this.asTime();
const model = yield this.increment(_id, Object.assign({}, $inc), Object.assign(Object.assign({}, $up), { updatedAt }));
//! make sure it has `_id`
model[this.idName] = _id;
return this.filters.afterUpdate(model);
});
}
/**
* save model by checking origin node.
* - use `doSave()` rather than `doUpdate()` for both create & update.
* - if `$create` is null, throw 404 error it if not found.
*
* @param type model-type
* @param id node-id
* @param node node to save (or update)
* @param $create (optional) initial creation model if not found.
*/
doSave(type, id, node, $create) {
return __awaiter(this, void 0, void 0, function* () {
//! read origin model w/o error.
const $org = (yield this.doRead(type, id, null).catch(e => {
if (`${e.message}`.startsWith('404 NOT FOUND'))
return null; // mark null to create later.
throw e;
}));
//! if `$create` is undefined, create it with default $key.
const _id = this.asKey(type, id);
const model = Object.assign({}, node); // copy from param.
model[this.idName] = _id; //! make sure the internal id
//! apply filter.
const $ups = this.filters.beforeSave(model, $org); //! `$org` should be null if create.
(0, engine_1._log)(NS, `> ${type}[${id}].update =`, engine_1.$U.json($ups));
//! if null, then nothing to update.
if (!$ups) {
const res = { [this.idName]: _id };
return res;
}
//! determine of create or update.
const { createdAt, updatedAt } = this.asTime();
if ($org) {
const $save = Object.assign(Object.assign({}, $ups), { updatedAt });
const res = yield this.doUpdate(type, id, $save);
return this.filters.afterSave(res, $org); //! `$org` should be valid if update.
}
else {
const $key = this.service.asKey$(type, id);
const $save = Object.assign(Object.assign(Object.assign(Object.assign({}, $ups), $create), $key), { createdAt, updatedAt: createdAt, deletedAt: 0 });
const res = yield this.storage.save(_id, $save);
return this.filters.afterSave(res, null); //! `$org` should be null if create.
}
});
}
/**
* lock data-entry by type+id w/ limited time tick
* - WARN! must release lock by `doRelease()`
*
* `total-waited-time = tick * interval (msec)`
*
* @param type model-type
* @param id model-id
* @param tick tick count to wait.
* @param interval timeout interval per each tick (in msec, default 1000 = 1sec)
*/
doLock(type, id, tick, interval) {
return __awaiter(this, void 0, void 0, function* () {
tick = engine_1.$U.N(tick, 30);
interval = engine_1.$U.N(interval, 1000);
if (typeof tick != 'number' || tick < 0)
throw new Error(` (${tick}) is not valid!`);
if (typeof interval != 'number' || interval < 1)
throw new Error(` (${interval}) is not valid!`);
const _id = this.asKey(type, id);
//! WARN! DO NOT MAKE ANY MODEL CREATION IN HERE.
// const $org = await this.storage.readOrCreate(_id, { lock: 0, ...$key } as any);
// _log(NS, `> $org[${type}/${id}].lock =`, $org.lock);
const thiz = this;
//! wait some time.
const wait = (timeout) => __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => {
setTimeout(() => {
resolve(timeout);
}, timeout);
});
});
const incLock = (_id, lock) => __awaiter(this, void 0, void 0, function* () {
const $up = {};
const $in = { lock };
return thiz.storage.update(_id, $up, $in).then($t2 => {
return engine_1.$U.N($t2.lock, 1);
});
});
//! recursive to wait lock()
const waitLock = (_id, ttl, int) => __awaiter(this, void 0, void 0, function* () {
//! try to check the current value.....
const lock = yield incLock(_id, 0).then(n => {
if (n > 1)
return n;
//! then, try to increment the lock
return incLock(_id, ttl > 0 ? 1 : 0);
});
(0, engine_1._log)(NS, `! waitLock(${_id}, ${ttl}). lock =`, lock);
if (lock == 1 || lock == 0) {
return true;
}
else if (ttl > 0 && lock > 1) {
return wait(int).then(() => waitLock(_id, ttl - 1, int));
}
else {
throw new Error(`400 TIMEOUT - model[${_id}].lock = ${lock}`);
}
});
return waitLock(_id, tick, interval);
});
}
/**
* release lock by resetting lock = 0.
*
* @param type model-type
* @param id model-id
*/
doRelease(type, id) {
return __awaiter(this, void 0, void 0, function* () {
(0, engine_1._log)(NS, `doRelease(${type}, ${id})... `);
const _id = this.asKey(type, id);
const $up = { lock: 0 };
const node = yield this.storage.update(_id, $up).catch(() => ({ lock: 0 }));
const lock = engine_1.$U.N(node.lock, 1);
return lock === 0 ? true : false;
});
}
/**
* create storage-service w/ fields list.
* - idName should be `_id`
*
* @param table table-name or dummy file name (ex: `dummy-data.yml`).
* @param fields required for dynamo table.
* @param idName internal partition-key name (default '_id')
*/
static makeStorageService(table, fields, idName) {
if (!table)
throw new Error(` (table-name) is required!`);
idName = idName === undefined ? '_id' : `${idName || ''}`;
//! clear the duplicated string
const clearDuplicated = (arr) => arr.sort().reduce((L, val) => {
if (val && L.indexOf(val) < 0)
L.push(val);
return L;
}, []);
//! make internal storage-service by table
if (table.endsWith('.yml')) {
return new storage_service_1.DummyStorageService(table, table.split('.')[0], idName);
}
else {
if (!fields)
throw new Error(` (list of field) is required!`);
fields = clearDuplicated(lemon_model_1.CORE_FIELDS.concat(fields));
return new storage_service_1.DynamoStorageService(table, fields, idName);
}
}
/**
* create proxy-storage-service by type
* @param type model-type
*/
makeTypedStorageService(type) {
if (!type)
throw new Error(` (model-type) is required!`);
// if (!fields) throw new Error(`@fields[${type}] (list of field) is required!`);
const res = new TypedStorageService(this, type);
return res;
}
}
exports.ProxyStorageService = ProxyStorageService;
ProxyStorageService.AUTO_SEQUENCE = 1000000;
ProxyStorageService.TYPE_SEQUENCE = 'sequence';
/**
* class: `TypedStorageService`
* - wrap id with type + id.
*/
// eslint-disable-next-line prettier/prettier
class TypedStorageService {
constructor(service, type) {
/**
* show self service name
*/
this.hello = () => `typed-storage-service:${this.type}/${this.storage.hello()}`;
/**
* get next auto-sequence id in number like `1000003`.
*/
this.nextId = () => this.storage.nextSeq(this.type);
/**
* get uuid like `d01764cd-9ef2-41e2-9e88-68e79555c979`
*/
this.nextUuid = () => this.storage.nextUuid(this.type);
/**
* read model by key + id with optional auto creation.
* - throws '404 NOT FOUND' if not found.
*
* @param id node-id
*/
this.read = (id) => this.storage.doRead(this.type, `${id || ''}`);
/**
* read model by key + id with optional auto creation.
*
* @param id node-id
* @param model initial model if not exist. (or throw 404 error)
*/
this.readOrCreate = (id, model) => this.storage.doRead(this.type, `${id || ''}`, model);
/**
* update model (or it will create automatically)
*
* @param id node-id
* @param model model to update
* @param incrementals (optional) fields to increment.
*/
this.update = (id, model, incrementals) => this.storage.doUpdate(this.type, `${id || ''}`, model, incrementals);
/**
* insert model w/ auto generated id
*
* @param model model to insert
*/
this.insert = (node) => __awaiter(this, void 0, void 0, function* () {
return this.nextId().then(_ => {
const id = `${_}`;
(0, engine_1._log)(NS, `> next-id[${this.type}] =`, id);
return this.readOrCreate(id, Object.assign(Object.assign({}, node), { id }));
});
});
/**
* update model (or it will create automatically)
*
* ```ts
* //before: { count: 1 };
* const res = await storage.increment(1, { count: 2 }, { total: 2 });
* //after : { count: 3, total: 2 }
* ```
*
* @param id node-id
* @param $increments model only with numbers
*/
this.increment = (id, $increments, $update) => this.storage.doIncrement(this.type, `${id || ''}`, $increments, $update);
/**
* delete model by id.
*
* @param id node-id
* @param destroy flag to destroy (real delete)
*/
this.delete = (id, destroy) => this.storage.doDelete(this.type, `${id || ''}`, destroy === undefined ? true : destroy);
/**
* save model by checking origin node.
* - use `doSave()` rather than `doUpdate()` for both create & update.
* - if `$create` is null, throw 404 error it if not found.
*
* @param id node-id
* @param node node to save (or update)
* @param $create (optional) initial creation model.
*/
this.save = (id, model, $create) => this.storage.doSave(this.type, `${id || ''}`, model, $create);
/**
* lock data-entry by type+id w/ limited time tick
* - WARN! must release lock by `release(id)`
*
* `total-waited-time = tick * interval (msec)`
*
* **[UPDATES]**
* 1. read original node (or, throw 404 error)
* 2. use internal lock.
*
* @param id model-id to lock
* @param tick tick count to wait.
* @param interval timeout interval per each tick (in msec, default 1000 = 1sec)
*/
this.lock = (id, tick, interval) => this.storage
.doRead(this.type, `${id || ''}`, null)
.then(node => this.storage.doLock(this.type, node.id, tick, interval));
/**
* release lock by resetting lock = 0.
* @param id model-id
*/
this.release = (id) => this.storage.doRelease(this.type, `${id || ''}`);
/**
* using `lock()`, guard func with auto lock & release.
*
* ```ts
* const res = await storage.guard(async ()=>{
* return 'abc';
* });
* // res === 'abc'
* ```
*/
this.guard = (id, handler, tick, interval) => __awaiter(this, void 0, void 0, function* () {
let locked = false;
return this.lock(id, tick, interval)
.then((_) => {
locked = _;
try {
return handler();
}
catch (e) {
return Promise.reject(e);
}
})
.then((_) => {
if (locked)
return this.release(id).then(() => _);
return _;
})
.catch((e) => {
if (locked)
return this.release(id).then(() => Promise.reject(e));
// throw e;
return Promise.reject(e);
});
});
/**
* make `UniqueFieldManager` for field.
*/
this.makeUniqueFieldManager = (field) => new UniqueFieldManager(this, field);
/**
* make `GeneralAPIController` for REST API w/ supporting basic CRUD
*/
this.makeGeneralAPIController = (search, uniqueField) => new general_api_controller_1.GeneralAPIController(this.type, this, search, uniqueField);
this.storage = service;
this.type = type;
}
}
exports.TypedStorageService = TypedStorageService;
/**
* class: `ModelUtil`
* - Helper functions for model.
*/
class ModelUtil {
}
exports.ModelUtil = ModelUtil;
ModelUtil.selfRead = (self, key, defValue) => {
const value = self[key];
return value === undefined ? defValue : value;
};
ModelUtil.selfPop = (self, key, defValue) => {
const value = ModelUtil.selfRead(self, key, defValue);
delete self[key];
return value;
};
/**
* attach `.pop()` method to object.
*
* ```js
* const data = CoreModelUtil.buildPop({'a':1});
* assert( 1 === data.pop('a) );
* const final = data.pop();
* assert( final == data );
*/
ModelUtil.buildPop = (thiz, popName = 'pop') => {
if (!thiz)
throw new Error('@thiz (object) is required!');
if (typeof thiz[popName] != 'undefined')
throw new Error(`.[${popName}] is duplicated!`);
thiz[popName] = function (key, defValue) {
if (!key) {
//! clear pop() if key is null.
delete this[popName];
return this;
}
else {
return ModelUtil.selfPop(this, key, defValue);
}
};
return thiz;
};
/**
* class: `UniqueFieldManager`
* - support `.{field}` is unique in typed-storage-service.
* - make lookup data entry to save the reverse mapping to origin id.
* - set `.stereo` as '#' to mark as lookup. (to filter out from Elastic.search())
* - set `.id` as `#{field}/{name}` or `#{name}`.
* - set `.meta` as origin id.
*/
class UniqueFieldManager {
constructor(storage, field = 'name') {
this.hello = () => `unique-field-manager:${this.type}/${this.field}:${this.storage.hello()}`;
this.type = storage.type;
this.storage = storage;
this.field = field;
}
/**
* validate value format
* - just check empty string.
* @param value unique value in same type+field.
*/
validate(value) {
const name2 = `${value || ''}`.trim();
return name2 && value == name2 ? true : false;
}
/**
* convert to internal id by value
* @param value unique value in same type group.
*/
asLookupId(value) {
return `#${this.field || ''}/${value || ''}`;
}
/**
* lookup model by value
* - use `.meta` property to link with the origin.
* - mark `.stereo` as to '#' to distinguish normal.
*
* @param value unique value in same type group.
* @param $creates (optional) create-set if not found.
*/
findOrCreate(value, $creates) {
return __awaiter(this, void 0, void 0, function* () {
if (!value || typeof value != 'string')
throw new Error(`@${this.field} (string) is required!`);
if (!this.validate(value))
throw new Error(`@${this.field} (${value || ''}) is not valid!`);
const ID = this.asLookupId(value);
const field = `${this.field}`;
if (!$creates) {
// STEP.1 read the origin name map
const $map = yield this.storage.read(ID).catch(test_helper_1.NUL404);
const rid = $map && $map.meta;
if (!rid)
throw new Error(`404 NOT FOUND - ${this.type}:${field}/${value}`);
// STEP.2 read the target node by stereo key.
const model = yield this.storage.read(rid);
return model;
}
else {
// STEP.0 validate if value is same
const $any = $creates || {};
if ($any[field] !== undefined && $any[field] !== value)
throw new Error(`@${this.field} (${value}) is not same as (${$any[field]})!`);
// STEP.1 read the origin value map
const $new = { stereo: '#', meta: `${$creates.id || ''}`, [field]: value };
const $map = yield this.storage.readOrCreate(ID, $new);
const rid = ($map && $map.meta) || $creates.id;
//! check if already saved, and id is differ.
if ($any['id'] && $any['id'] != rid)
throw new Error(` (${rid}) is not same as (${$any['id']})`);
// STEP.2 read the target node or create.
const $temp = Object.assign(Object.assign({}, $creates), { [field]: value });
const model = rid ? yield this.storage.readOrCreate(rid, $temp) : yield this.storage.insert($temp);
model[field] = value;
// STEP.3 update lookup key.
const newId = `${rid || model.id || ''}`;
if ($map.meta != newId) {
const $upt = { meta: newId };
yield this.storage.update(ID, $upt);
$map.meta = newId;
}
//! returns.
return model;
}
});
}
/**
* update lookup table (or create)
*
* @param model target model
* @param value (optional) new value of model.
*/
updateLookup(model, value) {
return __awaiter(this, void 0, void 0, function* () {
value = value || model[this.field];
if (!this.validate(value))
throw new Error(`@${this.field} (${value || ''}) is not valid!`);
const ID = this.asLookupId(value);
const field = `${this.field}`;
// STEP.0 validate if value has changed
const $any = model;
if ($any[field] && $any[field] !== value)
throw new Error(`@${this.field} (${value}) is not same as (${$any[field]})!`);
// STEP.1 check if value is duplicated.
const $org = yield this.storage.read(ID).catch(test_helper_1.NUL404);
const rid = $org && $org.meta;
if ($org && rid != model.id)
throw new Error(`400 DUPLICATED NAME - ${field}[${value}] is duplicated to ${this.type}[${rid}]`);
// STEP.2 save the name mapping.
const $new = Object.assign(Object.assign({}, model), { [field]: value, id: model.id });
return yield this.findOrCreate(value, $new);
});
}
}
exports.UniqueFieldManager = UniqueFieldManager;
//# sourceMappingURL=proxy-storage-service.js.map