UNPKG

@fabric-es/fabric-cqrs

Version:

Hyperledger Fabric middleware for event sourcing and cqrs pattern

347 lines 17 kB
"use strict"; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createQueryDatabase = void 0; const util_1 = __importDefault(require("util")); const debug_1 = __importDefault(require("debug")); const filter_1 = __importDefault(require("lodash/filter")); const groupBy_1 = __importDefault(require("lodash/groupBy")); const isEqual_1 = __importDefault(require("lodash/isEqual")); const types_1 = require("../types"); const utils_1 = require("../utils"); const constants_1 = require("./constants"); const model_1 = require("./model"); const _1 = require("."); const createQueryDatabase = (client, repos, { notifyExpiryBySec } = { notifyExpiryBySec: 86400 }) => { const debug = debug_1.default('queryHandler:createQueryDatabase'); const logger = utils_1.getLogger({ name: '[query-handler] createQueryDatabase.js', target: 'console' }); const commitRepo = _1.createRedisRepository('commit', { client, kind: 'commit', fields: model_1.commitSearchDefinition, preSelector: model_1.preSelector, postSelector: model_1.postSelector, }); const notificationCenter = _1.createNotificationCenter(client); const allRepos = Object.assign(repos, { commit: commitRepo }); const deleteItems = async (repo, pattern) => { const [errors, count] = await repo.deleteItemsByPattern(pattern); const isError = errors === null || errors === void 0 ? void 0 : errors.reduce((pre, cur) => pre || !!cur, false); return isError ? { status: 'ERROR', message: `${count} record(s) deleted`, errors, } : { status: 'OK', message: `${count} record(s) deleted`, data: count, }; }; const queryCommit = async (pattern, args) => { const [errors, data] = await commitRepo.queryCommitsByPattern(commitRepo.getPattern(pattern, args)); const isError = errors === null || errors === void 0 ? void 0 : errors.reduce((pre, cur) => pre || !!cur, false); return isError ? { status: 'ERROR', message: constants_1.QUERY_ERR, errors } : { status: 'OK', message: `${data.length} record(s) returned`, data }; }; const doSearch = async ({ repo, kind, query, param, countTotalOnly }) => { debug('doSearch:kind, %s', kind); debug('doSearch:kind, %s', query); const { search, getIndexName } = repo; const index = getIndexName(); const [errors, count, data] = await search({ countTotalOnly, kind, index, query, param, restoreFn: kind === 'entity' && repo.getPostSelector(), }); debug('doSearch:errors, %O', errors); const isError = errors === null || errors === void 0 ? void 0 : errors.reduce((pre, cur) => pre || !!cur, false); return isError ? { status: 'ERROR', message: 'search error', errors } : { status: 'OK', message: `${count} record(s) returned`, data: countTotalOnly ? count : data, }; }; return { clearNotification: async (option) => notificationCenter.clearNotification(option), clearNotifications: async (option) => notificationCenter.clearNotifications(option), deleteCommitByEntityId: async ({ entityName, id }) => { debug('deleteCommitByEntityId:id, %s', id); if (!entityName || !id) throw new Error(constants_1.INVALID_ARG); return deleteItems(commitRepo, commitRepo.getPattern('COMMITS_BY_ENTITYNAME_ENTITYID', [entityName, id])); }, deleteCommitByEntityName: async ({ entityName }) => { debug('deleteCommitByEntityName:entityName, %s', entityName); if (!entityName) throw new Error(constants_1.INVALID_ARG); return deleteItems(commitRepo, commitRepo.getPattern('COMMITS_BY_ENTITYNAME', [entityName])); }, deleteEntityByEntityName: async ({ entityName }) => { debug('deleteEntityByEntityName:entityName, %s', entityName); if (!entityName) throw new Error(constants_1.INVALID_ARG); const entityRepo = allRepos[entityName]; if (!entityRepo) throw new Error(`deleteEntityByEntityName: ${entityName} ${constants_1.REPO_NOT_FOUND}`); return deleteItems(entityRepo, entityRepo.getPattern('ENTITIES_BY_ENTITYNAME', [entityName])); }, fullTextSearchCommit: async ({ query, param, countTotalOnly }) => { debug('fullTextSearchCommit:query, %s', query); debug('fullTextSearchCommit:param, %O', param); if (!query) throw new Error(constants_1.INVALID_ARG); return doSearch({ repo: commitRepo, countTotalOnly, kind: 'commit', query, param, }); }, fullTextSearchEntity: async ({ entityName, query, param, countTotalOnly }) => { debug('fullTextSearchEntity:query, %s', query); debug('fullTextSearchEntity:param, %O', param); if (!query || !entityName) throw new Error(constants_1.INVALID_ARG); const repo = allRepos[entityName]; if (!repo) throw new Error(`fullTextSearchEntity: ${entityName} ${constants_1.REPO_NOT_FOUND} -- ${Object.keys(allRepos)}`); return doSearch({ repo, countTotalOnly, kind: 'entity', query, param }); }, getNotification: async ({ creator, entityName, id, commitId }) => notificationCenter.getNotification({ creator, entityName, id, commitId }), getNotificationsByFields: async ({ creator, entityName, id }) => notificationCenter.getNotificationsByFields({ creator, entityName, id }), getRedisCommitRepo: () => commitRepo, queryCommitByEntityId: async ({ entityName, id }) => { debug('queryCommitByEntityId:id, %s', id); if (!entityName || !id) throw new Error(constants_1.INVALID_ARG); return queryCommit('COMMITS_BY_ENTITYNAME_ENTITYID', [entityName, id]); }, queryCommitByEntityName: async ({ entityName }) => { debug('queryCommitByEntityName:entityName, %s', entityName); if (!entityName) throw new Error(constants_1.INVALID_ARG); return queryCommit('COMMITS_BY_ENTITYNAME', [entityName]); }, mergeCommit: async ({ commit }) => { debug('mergeCommit:commit', commit); if (!utils_1.isCommit(commit)) throw new Error(constants_1.INVALID_ARG); try { const key = allRepos['commit'].getKey(commit); debug('mergeCommit:key, %s', key); const status = await allRepos['commit'].hmset(commit); debug('mergeCommit:status, %s', status); return { status, message: `${key} merged successfully`, data: [key], }; } catch (e) { debug('mergeCommit:"try-catch-error"'); debug('mergeCommit, %O', e); logger.error(util_1.default.format('mergeCommit - %s, %j', constants_1.REDIS_ERR, e)); throw e; } }, mergeCommitBatch: async ({ entityName, commits }) => { var e_1, _a; debug('mergeCommitBatch starts'); if (!entityName || !commits) throw new Error(constants_1.INVALID_ARG); if (isEqual_1.default(commits, {})) return { status: 'OK', message: constants_1.NO_RECORDS, data: [], }; const data = []; const error = []; try { try { for (var _b = __asyncValues(Object.values(commits)), _c; _c = await _b.next(), !_c.done;) { const commit = _c.value; const status = await allRepos['commit'].hmset(commit); const key = allRepos['commit'].getKey(commit); if (status === 'OK') data.push(key); else error.push(key); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) await _a.call(_b); } finally { if (e_1) throw e_1.error; } } } catch (e) { debug('mergeCommitBatch:"try-catch-error"'); debug('mergeCommitBatch, %O', e); logger.error(util_1.default.format('mergeCommitBatch - %s, %j', constants_1.REDIS_ERR, e)); throw e; } debug('mergeCommitBatch:error, %O', error); return { status: error.length === 0 ? 'OK' : 'ERROR', message: `${data.length} record(s) merged successfully`, data, error, errors: error, }; }, mergeEntity: async ({ commit, reducer }) => { debug('mergeEntity:commit, %O', commit); const { entityName, entityId, commitId } = commit; const entityRepo = allRepos[entityName]; const entityKeyInRedis = allRepos[entityName].getKey(commit); const commitKeyInRedis = allRepos['commit'].getKey(commit); debug('mergeEntity:entityKeyInRedis, %s', entityKeyInRedis); debug('mergeEntity:commitKeyInRedis, %s', commitKeyInRedis); if (!entityRepo) throw new Error(`${constants_1.FATAL} mergeEntity: ${entityName} ${constants_1.REPO_NOT_FOUND}`); if (!utils_1.isCommit(commit) || !reducer) throw new Error(`${constants_1.FATAL} ${constants_1.INVALID_ARG}`); debug('mergeEntity:step-1'); const pattern = commitRepo.getPattern('COMMITS_BY_ENTITYNAME_ENTITYID', [ entityName, entityId, ]); debug('pattern: %s', pattern); const [errors, restoredCommits] = await commitRepo.queryCommitsByPattern(pattern); const isError = errors === null || errors === void 0 ? void 0 : errors.reduce((pre, cur) => pre || !!cur, false); debug('restoredCommits: %O', restoredCommits); if (isError) { debug('mergeEntity:errors, %O', errors); logger.error(util_1.default.format('errors of "await commitRepo.queryCommitsByPattern(pattern)", %j', errors)); return { status: 'ERROR', message: `${constants_1.FATAL} fail to retrieve existing commit`, errors, }; } debug('mergeEntity:step-2'); const history = [...restoredCommits, commit]; const { state, reduced } = types_1.computeEntity(history, reducer); debug('mergeEntity:history, %O', history); debug('mergeEntity:entity being merged, %O', state); if (reduced && !(state === null || state === void 0 ? void 0 : state.id)) { debug('mergeEntity:"fail to reduce"'); logger.error(util_1.default.format('state, %j', state)); return { status: 'ERROR', message: constants_1.REDUCE_ERR, errors: [new Error(`${constants_1.FATAL} fail to reduce, ${entityName}:${entityId}:${commitId}`)], }; } const data = []; try { let status; debug('mergeEntity:step-4'); if (reduced) { status = await allRepos[entityName].hmset(state, history); data.push({ key: entityKeyInRedis, status }); } debug('mergeEntity:step-5'); status = await allRepos['commit'].hmset(commit); data.push({ key: commitKeyInRedis, status }); debug('mergeEntity:step-6'); if (reduced) { await notificationCenter.notify({ creator: state._creator, entityName, id: entityId, commitId, }); } debug('mergeEntity:finish without error'); } catch (e) { if (!e.message.startsWith('[lifecycle]')) logger.error(util_1.default.format('mergeEntity - %s, %j', constants_1.REDIS_ERR, e)); throw e; } debug('data returns: %O', data); return { status: 'OK', message: `${entityKeyInRedis} merged successfully`, data }; }, mergeEntityBatch: async ({ entityName, commits, reducer }) => { var e_2, _a; debug('mergeEntityBatch starts'); if (!entityName || !commits || !reducer) throw new Error(constants_1.INVALID_ARG); const entityRepo = allRepos[entityName]; if (!entityRepo) throw new Error(`mergeEntityBatch: ${entityName} ${constants_1.REPO_NOT_FOUND}`); if (isEqual_1.default(commits, {})) return { status: 'OK', message: constants_1.NO_RECORDS, data: [], }; const filteredCommits = filter_1.default(commits, { entityName }); const groupByEntityId = groupBy_1.default(filteredCommits, ({ id }) => id); const errors = []; const entities = Object.entries(groupByEntityId) .map(([entityId, commits]) => { const { state, reduced } = types_1.computeEntity(commits, reducer); const keyOfEntityInRedis = allRepos[entityName].getKey(commits[0]); if (reduced && !state) errors.push(entityId); return { state, commits, key: keyOfEntityInRedis }; }) .filter(({ state }) => !!state); debug('mergeEntityBatch:errors, %O', errors); const data = []; try { for (var entities_1 = __asyncValues(entities), entities_1_1; entities_1_1 = await entities_1.next(), !entities_1_1.done;) { const { state, commits, key } = entities_1_1.value; try { const status = await allRepos[entityName].hmset(state, commits); data.push({ key, status }); } catch (e) { debug('mergeEntityBatch:e, %O', e); logger.error(util_1.default.format('mergeEntityBatch - %s, %j', constants_1.REDIS_ERR, e)); throw e; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (entities_1_1 && !entities_1_1.done && (_a = entities_1.return)) await _a.call(entities_1); } finally { if (e_2) throw e_2.error; } } debug('mergeEntityBatch:data, %O', data); return { status: errors.length === 0 ? 'OK' : 'ERROR', message: `${data.length} record(s) merged`, data, errors: errors.length === 0 ? null : errors, }; }, }; }; exports.createQueryDatabase = createQueryDatabase; //# sourceMappingURL=createQueryDatabase.js.map