@mbc-cqrs-serverless/core
Version:
CQRS and event base core
365 lines • 15.9 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var _a;
var CommandService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandService = void 0;
const common_1 = require("@nestjs/common");
const core_1 = require("@nestjs/core");
const util_1 = require("util");
const constants_1 = require("../constants");
const user_1 = require("../context/user");
const dynamodb_service_1 = require("../data-store/dynamodb.service");
const decorators_1 = require("../decorators");
const helpers_1 = require("../helpers");
const key_1 = require("../helpers/key");
const sns_service_1 = require("../queue/sns.service");
const services_1 = require("../services");
const command_module_definition_1 = require("./command.module-definition");
const data_service_1 = require("./data.service");
const enums_1 = require("./enums");
const data_sync_dds_handler_1 = require("./handlers/data-sync-dds.handler");
const ttl_service_1 = require("./ttl.service");
const TABLE_NAME = Symbol('command');
const DATA_SYNC_HANDLER = Symbol(decorators_1.DATA_SYNC_HANDLER_METADATA);
let CommandService = CommandService_1 = class CommandService {
constructor(options, dynamoDbService, explorerService, moduleRef, snsService, dataSyncDdsHandler, dataService, ttlService) {
this.options = options;
this.dynamoDbService = dynamoDbService;
this.explorerService = explorerService;
this.moduleRef = moduleRef;
this.snsService = snsService;
this.dataSyncDdsHandler = dataSyncDdsHandler;
this.dataService = dataService;
this.ttlService = ttlService;
this[_a] = [];
this.tableName = this.dynamoDbService.getTableName(this.options.tableName, enums_1.TableType.COMMAND);
this.logger = new common_1.Logger(`${CommandService_1.name}:${this.tableName}`);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
publishItem(key) {
throw new Error('Method not implemented.');
}
onModuleInit() {
if (!this.options.disableDefaultHandler) {
this[DATA_SYNC_HANDLER] = [this.dataSyncDdsHandler];
}
if (this.options.dataSyncHandlers?.length) {
// this.logger.debug('init data sync handlers')
this[DATA_SYNC_HANDLER].push(...this.options.dataSyncHandlers.map((HandlerClass) => this.moduleRef.get(HandlerClass, { strict: false })));
}
this.logger.debug('find data sync handlers from decorator');
const { dataSyncHandlers } = this.explorerService.exploreDataSyncHandlers(this.options.tableName);
this[DATA_SYNC_HANDLER].push(...dataSyncHandlers
.map((handler) => this.moduleRef.get(handler, { strict: false }))
.filter((handler) => !!handler));
// this.logger.debug(
// 'data sync handlers length: ' + this[DATA_SYNC_HANDLER].length,
// )
}
set tableName(name) {
this[TABLE_NAME] = name;
}
get tableName() {
return this[TABLE_NAME];
}
get dataSyncHandlers() {
// this.logger.debug(
// 'dataSyncHandlers size:: ' + this[DATA_SYNC_HANDLER].length,
// )
return this[DATA_SYNC_HANDLER];
}
getDataSyncHandler(name) {
return this.dataSyncHandlers.find((handler) => handler.constructor.name === name);
}
async publishPartialUpdateSync(input, options) {
const item = await this.dataService.getItem({
pk: input.pk,
sk: input.sk,
});
if (!item || item.version !== input.version) {
throw new common_1.BadRequestException('Invalid input: item not found or version mismatch');
}
if (!Object.keys(input).includes('ttl')) {
delete item.ttl;
}
const fullInput = (0, helpers_1.mergeDeep)({}, item, input, { version: item.version });
this.logger.debug('publishPartialUpdateSync::', fullInput);
return await this.publishSync(fullInput, options);
}
async publishPartialUpdateAsync(input, options) {
let item;
if (input.version > constants_1.VERSION_FIRST) {
item = await this.getItem({
pk: input.pk,
sk: (0, key_1.addSortKeyVersion)(input.sk, input.version),
});
}
else {
item = await this.getLatestItem({
pk: input.pk,
sk: (0, key_1.removeSortKeyVersion)(input.sk),
});
}
if (!item) {
throw new common_1.BadRequestException('Invalid input key: item not found');
}
if (!Object.keys(input).includes('ttl')) {
delete item.ttl;
}
const fullInput = (0, helpers_1.mergeDeep)({}, item, input, { version: item.version });
this.logger.debug('publishPartialUpdate::', fullInput);
return await this.publishAsync(fullInput, options);
}
async publishSync(input, options) {
const item = await this.dataService.getItem({ pk: input.pk, sk: input.sk });
let inputVersion = input.version ?? constants_1.VERSION_FIRST;
if (inputVersion === constants_1.VERSION_LATEST) {
inputVersion = item?.version ?? constants_1.VERSION_FIRST;
}
else if ((item?.version ?? 0) !== inputVersion) {
throw new common_1.BadRequestException('Invalid input version. The input version must be equal to the latest version');
}
const userContext = (0, user_1.getUserContext)(options.invokeContext);
const requestId = options?.requestId || options.invokeContext?.context?.awsRequestId;
const sourceIp = options.invokeContext?.event?.requestContext?.http?.sourceIp;
const version = (item?.version ?? inputVersion) + 1;
const command = {
ttl: await this.ttlService.calculateTtl(enums_1.TableType.DATA, (0, key_1.getTenantCode)(input.pk)),
...input,
version,
source: options?.source,
requestId,
createdAt: new Date(),
updatedAt: item?.updatedAt ?? new Date(),
createdBy: userContext.userId,
updatedBy: item?.updatedBy ?? userContext.userId,
createdIp: sourceIp,
updatedIp: item?.updatedIp ?? sourceIp,
};
this.logger.debug('publishSync::', command);
await this.dataService.publish(command);
const targetSyncHandlers = this.dataSyncHandlers?.filter((handler) => handler.type !== 'dynamodb');
await Promise.all(targetSyncHandlers.map((handler) => handler.up(command)));
return command;
}
async publishAsync(input, options) {
let inputVersion = input.version || constants_1.VERSION_FIRST;
let item;
if (inputVersion === constants_1.VERSION_LATEST) {
item = await this.getLatestItem({
pk: input.pk,
sk: (0, key_1.removeSortKeyVersion)(input.sk),
});
inputVersion = item?.version || constants_1.VERSION_FIRST;
}
else if (inputVersion > constants_1.VERSION_FIRST) {
// check current version
item = await this.getItem({
pk: input.pk,
sk: (0, key_1.addSortKeyVersion)(input.sk, inputVersion),
});
if (!item) {
throw new common_1.BadRequestException('Invalid input version. The input version must be equal to the latest version');
}
}
if (item && this.isNotCommandDirty(item, input)) {
// do not update if command is not dirty
return null;
}
const userContext = (0, user_1.getUserContext)(options.invokeContext);
const requestId = options?.requestId || options.invokeContext?.context?.awsRequestId;
const sourceIp = options.invokeContext?.event?.requestContext?.http?.sourceIp;
const version = inputVersion + 1;
const command = {
ttl: await this.ttlService.calculateTtl(enums_1.TableType.DATA, (0, key_1.getTenantCode)(input.pk)),
...input,
sk: (0, key_1.addSortKeyVersion)(input.sk, version),
version,
source: options?.source,
requestId,
createdAt: new Date(),
updatedAt: new Date(),
createdBy: userContext.userId,
updatedBy: userContext.userId,
createdIp: sourceIp,
updatedIp: sourceIp,
};
this.logger.debug('publish::', command);
await this.dynamoDbService.putItem(this.tableName, command, 'attribute_not_exists(pk) AND attribute_not_exists(sk)');
return command;
}
async duplicate(key, options) {
const item = await this.getItem(key);
if (!item) {
throw new common_1.BadRequestException('Invalid input key: item not found');
}
const userContext = (0, user_1.getUserContext)(options.invokeContext);
item.version += 1;
item.sk = (0, key_1.addSortKeyVersion)(item.sk, item.version);
item.source = 'duplicated';
item.requestId =
options?.requestId || options.invokeContext?.context?.awsRequestId;
item.updatedAt = new Date();
item.updatedBy = userContext.userId;
item.updatedIp =
options.invokeContext?.event?.requestContext?.http?.sourceIp;
this.logger.debug('duplicate::', item);
await this.dynamoDbService.putItem(this.tableName, item, 'attribute_not_exists(pk) AND attribute_not_exists(sk)');
return item;
}
async reSyncData() {
const targetSyncHandlers = this.dataSyncHandlers?.filter((handler) => handler.type !== 'dynamodb');
if (!targetSyncHandlers?.length) {
this.logger.debug('no data sync handlers');
return;
}
const dataTableName = this.dataService.tableName;
let startKey = undefined;
while (true) {
const res = await this.dynamoDbService.listAllItems(dataTableName, startKey);
if (res?.items?.length) {
for (const item of res.items) {
item.sk = (0, key_1.addSortKeyVersion)(item.sk, item.version);
for (const handler of targetSyncHandlers) {
await handler.up(item);
}
}
}
startKey = res?.lastKey;
if (!startKey) {
break;
}
}
}
async updateStatus(key, status, notifyId) {
const item = await this.dynamoDbService.updateItem(this.tableName, key, {
set: { status },
});
// notification via SNS
await this.snsService.publish({
action: 'command-status',
...key,
table: this.tableName,
id: notifyId || `${this.tableName}#${key.pk}#${key.sk}`,
tenantCode: key.pk.substring(key.pk.indexOf('#') + 1),
content: { status, source: item?.source },
});
}
async getItem(key) {
if (!key.sk.includes(constants_1.VER_SEPARATOR)) {
return this.getLatestItem(key);
}
return await this.dynamoDbService.getItem(this.tableName, key);
}
async getLatestItem(key) {
const lookUpStep = 5;
const dataItem = await this.dataService.getItem(key);
let ver = (dataItem?.version || 0) + lookUpStep;
let isUp = true;
while (true) {
if (ver <= constants_1.VERSION_FIRST) {
return null;
}
const item = await this.getItem({
pk: key.pk,
sk: (0, key_1.addSortKeyVersion)(key.sk, ver),
});
if (item) {
if (!isUp) {
// look down
return item;
}
// continue look up
ver += lookUpStep;
}
else {
// look down
ver -= 1;
isUp = false;
}
}
}
isNotCommandDirty(item, input) {
const comparedKeys = [
'id',
'code',
'name',
'tenantCode',
'type',
'isDeleted',
'seq',
'ttl',
'attributes',
];
const itemPicked = structuredClone((0, helpers_1.pickKeys)(item, comparedKeys));
const inputPicked = structuredClone((0, helpers_1.pickKeys)(input, comparedKeys));
// Normalize null/undefined: treat missing keys as null for comparison
for (const key of comparedKeys) {
if (!(key in itemPicked))
itemPicked[key] = null;
if (!(key in inputPicked))
inputPicked[key] = null;
}
return (0, util_1.isDeepStrictEqual)(itemPicked, inputPicked);
}
async updateTtl(key) {
const version = (0, key_1.getSortKeyVersion)(key.sk);
const sk = (0, key_1.removeSortKeyVersion)(key.sk);
if (version <= constants_1.VERSION_FIRST + 1) {
return null;
}
const previousSk = (0, key_1.addSortKeyVersion)(sk, version - 1);
const command = await this.dynamoDbService.getItem(this.tableName, {
pk: key.pk,
sk: previousSk,
});
if (!command) {
return null;
}
command.sk = previousSk;
const ttl = await this.ttlService.calculateTtl(enums_1.TableType.COMMAND, (0, key_1.getTenantCode)(key.pk));
command.ttl = ttl;
this.logger.debug('updateTtl::', command);
return await this.dynamoDbService.putItem(this.tableName, command);
}
async updateTaskToken(key, token) {
this.logger.debug(`Saving taskToken for ${key.pk}#${key.sk}`);
return await this.dynamoDbService.updateItem(this.tableName, key, {
set: { taskToken: token },
});
}
async getNextCommand(currentKey) {
this.logger.debug(`Getting next command for ${currentKey.pk}#${currentKey.sk}`);
const nextKey = {
pk: currentKey.pk,
sk: (0, key_1.addSortKeyVersion)((0, key_1.removeSortKeyVersion)(currentKey.sk), (0, key_1.getSortKeyVersion)(currentKey.sk) + 1),
};
return await this.dynamoDbService.getItem(this.tableName, nextKey);
}
};
exports.CommandService = CommandService;
_a = DATA_SYNC_HANDLER;
exports.CommandService = CommandService = CommandService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)(command_module_definition_1.MODULE_OPTIONS_TOKEN)),
__metadata("design:paramtypes", [Object, dynamodb_service_1.DynamoDbService,
services_1.ExplorerService,
core_1.ModuleRef,
sns_service_1.SnsService,
data_sync_dds_handler_1.DataSyncDdsHandler,
data_service_1.DataService,
ttl_service_1.TtlService])
], CommandService);
//# sourceMappingURL=command.service.js.map