UNPKG

lemon-core

Version:
913 lines 36.5 kB
"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(`@stepNext[${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 (${tick}) is not valid!`); if (typeof interval != 'number' || interval < 1) throw new Error(`@interval (${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 (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(`@fields (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(`@type (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(`@id (${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