Files
ai-code-app/etc/servers/work-server/src/services/notification-message-service.js

390 lines
21 KiB
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());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.notificationMessageReadPayloadSchema = exports.notificationMessagePayloadSchema = exports.notificationMessageListQuerySchema = exports.NOTIFICATION_MESSAGE_TABLE = void 0;
exports.ensureNotificationMessagesTable = ensureNotificationMessagesTable;
exports.listNotificationMessages = listNotificationMessages;
exports.getNotificationMessage = getNotificationMessage;
exports.createNotificationMessage = createNotificationMessage;
exports.selectNotificationMessageIdsToDelete = selectNotificationMessageIdsToDelete;
exports.deleteOlderNotificationMessagesBySource = deleteOlderNotificationMessagesBySource;
exports.updateNotificationMessageReadState = updateNotificationMessageReadState;
exports.deleteNotificationMessage = deleteNotificationMessage;
var zod_1 = require("zod");
var client_js_1 = require("../db/client.js");
exports.NOTIFICATION_MESSAGE_TABLE = 'notification_messages';
var notificationMessagePrioritySchema = zod_1.z.enum(['low', 'normal', 'high', 'urgent']);
var notificationMessageListStatusSchema = zod_1.z.enum(['all', 'unread']);
exports.notificationMessageListQuerySchema = zod_1.z.object({
status: notificationMessageListStatusSchema.default('all'),
limit: zod_1.z.coerce.number().int().min(1).max(100).default(20),
});
exports.notificationMessagePayloadSchema = zod_1.z.object({
title: zod_1.z.string().trim().min(1).max(200),
body: zod_1.z.string().trim().min(1).max(20000),
category: zod_1.z.string().trim().min(1).max(60).default('general'),
source: zod_1.z.string().trim().min(1).max(80).default('system'),
priority: notificationMessagePrioritySchema.default('normal'),
metadata: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).default({}),
});
exports.notificationMessageReadPayloadSchema = zod_1.z.object({
read: zod_1.z.boolean().default(true),
});
function normalizePreviewText(value) {
var normalized = value
.replace(/```[\s\S]*?```/g, ' ')
.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
.replace(/[#>*_`~-]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return normalized.length > 140 ? "".concat(normalized.slice(0, 137).trimEnd(), "...") : normalized;
}
function mapNotificationMessageRow(row) {
var _a, _b, _c, _d, _e, _f, _g;
var body = String((_a = row.body) !== null && _a !== void 0 ? _a : '');
var metadata = typeof row.metadata_json === 'object' && row.metadata_json ? row.metadata_json : {};
var metadataPreview = typeof metadata.previewText === 'string'
? metadata.previewText
: typeof metadata.listPreviewText === 'string'
? metadata.listPreviewText
: '';
return {
id: Number((_b = row.id) !== null && _b !== void 0 ? _b : 0),
title: String((_c = row.title) !== null && _c !== void 0 ? _c : ''),
body: body,
preview: normalizePreviewText(metadataPreview || body),
category: String((_d = row.category) !== null && _d !== void 0 ? _d : 'general'),
source: String((_e = row.source) !== null && _e !== void 0 ? _e : 'system'),
priority: notificationMessagePrioritySchema.catch('normal').parse(row.priority),
read: Boolean(row.is_read),
readAt: row.read_at === null || row.read_at === undefined ? null : String(row.read_at),
metadata: metadata,
createdAt: String((_f = row.created_at) !== null && _f !== void 0 ? _f : ''),
updatedAt: String((_g = row.updated_at) !== null && _g !== void 0 ? _g : ''),
};
}
function resolveInsertedId(result) {
if (typeof result === 'number' && Number.isInteger(result) && result > 0) {
return result;
}
if (Array.isArray(result)) {
var first = result[0];
if (typeof first === 'number' && Number.isInteger(first) && first > 0) {
return first;
}
if (first && typeof first === 'object' && 'id' in first) {
var id = Number(first.id);
return Number.isInteger(id) && id > 0 ? id : null;
}
}
if (result && typeof result === 'object' && 'id' in result) {
var id = Number(result.id);
return Number.isInteger(id) && id > 0 ? id : null;
}
return null;
}
function supportsReturning() {
var _a;
var clientName = String((_a = client_js_1.db.client.config.client) !== null && _a !== void 0 ? _a : '').toLowerCase();
return ['pg', 'postgres', 'postgresql', 'sqlite3', 'better-sqlite3', 'oracledb', 'mssql'].includes(clientName);
}
function ensureNotificationMessagesTable() {
return __awaiter(this, void 0, void 0, function () {
var hasTable, requiredColumns, _loop_1, _i, requiredColumns_1, _a, columnName, createColumn;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, client_js_1.db.schema.hasTable(exports.NOTIFICATION_MESSAGE_TABLE)];
case 1:
hasTable = _b.sent();
if (!!hasTable) return [3 /*break*/, 3];
return [4 /*yield*/, client_js_1.db.schema.createTable(exports.NOTIFICATION_MESSAGE_TABLE, function (table) {
table.increments('id').primary();
table.string('title', 200).notNullable();
table.text('body').notNullable();
table.string('category', 60).notNullable().defaultTo('general');
table.string('source', 80).notNullable().defaultTo('system');
table.string('priority', 20).notNullable().defaultTo('normal');
table.boolean('is_read').notNullable().defaultTo(false);
table.timestamp('read_at', { useTz: true }).nullable();
table.jsonb('metadata_json').notNullable().defaultTo('{}');
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now());
table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now());
})];
case 2:
_b.sent();
_b.label = 3;
case 3:
requiredColumns = [
['title', function (table) { return table.string('title', 200).notNullable().defaultTo('알림'); }],
['body', function (table) { return table.text('body').notNullable().defaultTo(''); }],
['category', function (table) { return table.string('category', 60).notNullable().defaultTo('general'); }],
['source', function (table) { return table.string('source', 80).notNullable().defaultTo('system'); }],
['priority', function (table) { return table.string('priority', 20).notNullable().defaultTo('normal'); }],
['is_read', function (table) { return table.boolean('is_read').notNullable().defaultTo(false); }],
['read_at', function (table) { return table.timestamp('read_at', { useTz: true }).nullable(); }],
['metadata_json', function (table) { return table.jsonb('metadata_json').notNullable().defaultTo('{}'); }],
['created_at', function (table) { return table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now()); }],
['updated_at', function (table) { return table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now()); }],
];
_loop_1 = function (columnName, createColumn) {
var hasColumn;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, client_js_1.db.schema.hasColumn(exports.NOTIFICATION_MESSAGE_TABLE, columnName)];
case 1:
hasColumn = _c.sent();
if (!!hasColumn) return [3 /*break*/, 3];
return [4 /*yield*/, client_js_1.db.schema.alterTable(exports.NOTIFICATION_MESSAGE_TABLE, function (table) {
createColumn(table);
})];
case 2:
_c.sent();
_c.label = 3;
case 3: return [2 /*return*/];
}
});
};
_i = 0, requiredColumns_1 = requiredColumns;
_b.label = 4;
case 4:
if (!(_i < requiredColumns_1.length)) return [3 /*break*/, 7];
_a = requiredColumns_1[_i], columnName = _a[0], createColumn = _a[1];
return [5 /*yield**/, _loop_1(columnName, createColumn)];
case 5:
_b.sent();
_b.label = 6;
case 6:
_i++;
return [3 /*break*/, 4];
case 7: return [2 /*return*/];
}
});
});
}
function listNotificationMessages(query) {
return __awaiter(this, void 0, void 0, function () {
var parsedQuery, builder, rows, unreadCountResult;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, ensureNotificationMessagesTable()];
case 1:
_b.sent();
parsedQuery = exports.notificationMessageListQuerySchema.parse(query);
builder = (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE)
.select('*')
.orderBy('is_read', 'asc')
.orderBy('created_at', 'desc')
.orderBy('id', 'desc')
.limit(parsedQuery.limit);
if (parsedQuery.status === 'unread') {
builder.where({ is_read: false });
}
return [4 /*yield*/, builder];
case 2:
rows = _b.sent();
return [4 /*yield*/, (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE)
.where({ is_read: false })
.count({ count: '*' })
.first()];
case 3:
unreadCountResult = _b.sent();
return [2 /*return*/, {
items: rows.map(function (row) { return mapNotificationMessageRow(row); }),
unreadCount: Number((_a = unreadCountResult === null || unreadCountResult === void 0 ? void 0 : unreadCountResult.count) !== null && _a !== void 0 ? _a : 0),
}];
}
});
});
}
function getNotificationMessage(id) {
return __awaiter(this, void 0, void 0, function () {
var row;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ensureNotificationMessagesTable()];
case 1:
_a.sent();
return [4 /*yield*/, (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE).where({ id: id }).first()];
case 2:
row = _a.sent();
return [2 /*return*/, row ? mapNotificationMessageRow(row) : null];
}
});
});
}
function createNotificationMessage(payload) {
return __awaiter(this, void 0, void 0, function () {
var parsedPayload, insertQuery, insertResult, _a, insertedId, row;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, ensureNotificationMessagesTable()];
case 1:
_b.sent();
parsedPayload = exports.notificationMessagePayloadSchema.parse(payload);
insertQuery = (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE).insert({
title: parsedPayload.title,
body: parsedPayload.body,
category: parsedPayload.category,
source: parsedPayload.source,
priority: parsedPayload.priority,
metadata_json: parsedPayload.metadata,
is_read: false,
read_at: null,
created_at: client_js_1.db.fn.now(),
updated_at: client_js_1.db.fn.now(),
});
if (!supportsReturning()) return [3 /*break*/, 3];
return [4 /*yield*/, insertQuery.returning('id')];
case 2:
_a = _b.sent();
return [3 /*break*/, 5];
case 3: return [4 /*yield*/, insertQuery];
case 4:
_a = _b.sent();
_b.label = 5;
case 5:
insertResult = _a;
insertedId = resolveInsertedId(insertResult);
if (!insertedId) {
throw new Error('알림 메시지 저장 후 ID를 확인하지 못했습니다.');
}
return [4 /*yield*/, (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE).where({ id: insertedId }).first()];
case 6:
row = _b.sent();
if (!row) {
throw new Error('저장된 알림 메시지를 다시 불러오지 못했습니다.');
}
return [2 /*return*/, mapNotificationMessageRow(row)];
}
});
});
}
function selectNotificationMessageIdsToDelete(items, keepLatestCount) {
if (keepLatestCount === void 0) { keepLatestCount = 1; }
var normalizedKeepLatestCount = Number.isInteger(keepLatestCount) && keepLatestCount > 0 ? keepLatestCount : 1;
return items
.slice()
.sort(function (left, right) {
var leftCreatedAt = Date.parse(left.createdAt !== null && left.createdAt !== void 0 ? left.createdAt : '');
var rightCreatedAt = Date.parse(right.createdAt !== null && right.createdAt !== void 0 ? right.createdAt : '');
if (Number.isFinite(leftCreatedAt) && Number.isFinite(rightCreatedAt) && leftCreatedAt !== rightCreatedAt) {
return rightCreatedAt - leftCreatedAt;
}
return right.id - left.id;
})
.slice(normalizedKeepLatestCount)
.map(function (item) { return item.id; });
}
function deleteOlderNotificationMessagesBySource(source, keepLatestCount) {
if (keepLatestCount === void 0) { keepLatestCount = 1; }
return __awaiter(this, void 0, void 0, function () {
var normalizedSource, rows, idsToDelete;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ensureNotificationMessagesTable()];
case 1:
_a.sent();
normalizedSource = source.trim();
if (!normalizedSource) {
return [2 /*return*/, 0];
}
return [4 /*yield*/, (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE)
.select('id', 'created_at')
.where({ source: normalizedSource })];
case 2:
rows = _a.sent();
idsToDelete = selectNotificationMessageIdsToDelete(rows.map(function (row) { return ({
id: Number(row.id !== null && row.id !== void 0 ? row.id : 0),
createdAt: row.created_at === null || row.created_at === undefined ? null : String(row.created_at),
}); }), keepLatestCount).filter(function (id) { return Number.isInteger(id) && id > 0; });
if (idsToDelete.length === 0) {
return [2 /*return*/, 0];
}
return [2 /*return*/, (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE).whereIn('id', idsToDelete).del()];
}
});
});
}
function updateNotificationMessageReadState(id, payload) {
return __awaiter(this, void 0, void 0, function () {
var parsedPayload, updatedCount, row;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ensureNotificationMessagesTable()];
case 1:
_a.sent();
parsedPayload = exports.notificationMessageReadPayloadSchema.parse(payload);
return [4 /*yield*/, (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE)
.where({ id: id })
.update({
is_read: parsedPayload.read,
read_at: parsedPayload.read ? client_js_1.db.fn.now() : null,
updated_at: client_js_1.db.fn.now(),
})];
case 2:
updatedCount = _a.sent();
if (!updatedCount) {
return [2 /*return*/, null];
}
return [4 /*yield*/, (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE).where({ id: id }).first()];
case 3:
row = _a.sent();
return [2 /*return*/, row ? mapNotificationMessageRow(row) : null];
}
});
});
}
function deleteNotificationMessage(id) {
return __awaiter(this, void 0, void 0, function () {
var deletedCount;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ensureNotificationMessagesTable()];
case 1:
_a.sent();
return [4 /*yield*/, (0, client_js_1.db)(exports.NOTIFICATION_MESSAGE_TABLE).where({ id: id }).del()];
case 2:
deletedCount = _a.sent();
return [2 /*return*/, deletedCount > 0];
}
});
});
}