chore: test deploy snapshot

This commit is contained in:
2026-05-27 14:40:33 +09:00
parent 58c5a7cfee
commit e8a628ac34
5 changed files with 2637 additions and 461 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,312 @@
import { db } from '../db/client.js';
import {
CHAT_CONVERSATION_TABLE,
ensureChatConversationTables,
} from './chat-room-service.js';
const CHAT_SHARE_TOKEN_ROOM_MAP_TABLE = 'chat_share_token_room_maps';
export type ChatShareTokenRoomMapItem = {
tokenId: string;
sessionId: string;
rootRequestId: string;
isDefault: boolean;
sortOrder: number;
createdByClientId: string | null;
title: string;
requestBadgeLabel: string | null;
chatTypeId: string | null;
lastChatTypeId: string | null;
contextLabel: string | null;
contextDescription: string | null;
notifyOffline: boolean;
createdAt: string | null;
updatedAt: string | null;
conversationUpdatedAt: string | null;
};
function normalizeOptionalText(value: unknown) {
if (typeof value !== 'string') {
return null;
}
const normalized = value.trim();
return normalized || null;
}
function normalizeRequiredText(value: unknown) {
if (typeof value !== 'string') {
return '';
}
return value.trim();
}
function normalizeBoolean(value: unknown) {
return value === true;
}
function normalizeInteger(value: unknown, fallback = 0) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return fallback;
}
return Math.trunc(parsed);
}
function normalizeDateTime(value: unknown) {
if (value == null) {
return null;
}
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? null : value.toISOString();
}
if (typeof value === 'string') {
const normalized = value.trim();
return normalized || null;
}
return null;
}
function mapChatShareTokenRoomRow(row: Record<string, unknown>): ChatShareTokenRoomMapItem {
return {
tokenId: normalizeRequiredText(row.shared_resource_token_id),
sessionId: normalizeRequiredText(row.session_id),
rootRequestId: normalizeRequiredText(row.root_request_id),
isDefault: normalizeBoolean(row.is_default),
sortOrder: normalizeInteger(row.sort_order),
createdByClientId: normalizeOptionalText(row.created_by_client_id),
title: normalizeRequiredText(row.title) || '공유 채팅방',
requestBadgeLabel: normalizeOptionalText(row.request_badge_label),
chatTypeId: normalizeOptionalText(row.chat_type_id),
lastChatTypeId: normalizeOptionalText(row.last_chat_type_id),
contextLabel: normalizeOptionalText(row.context_label),
contextDescription: normalizeOptionalText(row.context_description),
notifyOffline: normalizeBoolean(row.notify_offline),
createdAt: normalizeDateTime(row.created_at),
updatedAt: normalizeDateTime(row.updated_at),
conversationUpdatedAt: normalizeDateTime(row.conversation_updated_at),
};
}
export async function ensureChatShareTokenRoomMapTable() {
await ensureChatConversationTables();
const hasTable = await db.schema.hasTable(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE);
if (!hasTable) {
await db.schema.createTable(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE, (table) => {
table.increments('id').primary();
table.string('shared_resource_token_id', 120).notNullable().index();
table.string('session_id', 120).notNullable().index();
table.string('root_request_id', 120).notNullable();
table.boolean('is_default').notNullable().defaultTo(false);
table.integer('sort_order').notNullable().defaultTo(0);
table.string('created_by_client_id', 120).nullable();
table.timestamp('archived_at', { useTz: true }).nullable().index();
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
table.unique(['shared_resource_token_id', 'session_id']);
});
}
const requiredColumns: Array<[string, (table: any) => void]> = [
['shared_resource_token_id', (table) => table.string('shared_resource_token_id', 120).notNullable().index()],
['session_id', (table) => table.string('session_id', 120).notNullable().index()],
['root_request_id', (table) => table.string('root_request_id', 120).notNullable().defaultTo('')],
['is_default', (table) => table.boolean('is_default').notNullable().defaultTo(false)],
['sort_order', (table) => table.integer('sort_order').notNullable().defaultTo(0)],
['created_by_client_id', (table) => table.string('created_by_client_id', 120).nullable()],
['archived_at', (table) => table.timestamp('archived_at', { useTz: true }).nullable().index()],
['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
];
for (const [columnName, createColumn] of requiredColumns) {
const hasColumn = await db.schema.hasColumn(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE, columnName);
if (!hasColumn) {
await db.schema.alterTable(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE, (table) => {
createColumn(table);
});
}
}
}
export async function listChatShareTokenRoomMaps(tokenId: string) {
const normalizedTokenId = tokenId.trim();
if (!normalizedTokenId) {
return [] as ChatShareTokenRoomMapItem[];
}
await ensureChatShareTokenRoomMapTable();
const rows = await db(`${CHAT_SHARE_TOKEN_ROOM_MAP_TABLE} as room_map`)
.leftJoin(`${CHAT_CONVERSATION_TABLE} as conversation`, 'conversation.session_id', 'room_map.session_id')
.select(
'room_map.shared_resource_token_id',
'room_map.session_id',
'room_map.root_request_id',
'room_map.is_default',
'room_map.sort_order',
'room_map.created_by_client_id',
'room_map.created_at',
'room_map.updated_at',
'conversation.title',
'conversation.request_badge_label',
'conversation.chat_type_id',
'conversation.last_chat_type_id',
'conversation.context_label',
'conversation.context_description',
'conversation.notify_offline',
'conversation.updated_at as conversation_updated_at',
)
.where({ 'room_map.shared_resource_token_id': normalizedTokenId })
.whereNull('room_map.archived_at')
.orderBy('room_map.is_default', 'desc')
.orderBy('room_map.sort_order', 'asc')
.orderBy('room_map.created_at', 'asc');
return rows.map((row) => mapChatShareTokenRoomRow(row));
}
export async function getChatShareTokenRoomMap(tokenId: string, sessionId: string) {
const normalizedTokenId = tokenId.trim();
const normalizedSessionId = sessionId.trim();
if (!normalizedTokenId || !normalizedSessionId) {
return null;
}
const rooms = await listChatShareTokenRoomMaps(normalizedTokenId);
return rooms.find((item) => item.sessionId === normalizedSessionId) ?? null;
}
export async function upsertChatShareTokenRoomMap(args: {
tokenId: string;
sessionId: string;
rootRequestId: string;
isDefault?: boolean;
sortOrder?: number | null;
createdByClientId?: string | null;
}) {
const normalizedTokenId = args.tokenId.trim();
const normalizedSessionId = args.sessionId.trim();
const normalizedRootRequestId = args.rootRequestId.trim();
if (!normalizedTokenId || !normalizedSessionId || !normalizedRootRequestId) {
return null;
}
await ensureChatShareTokenRoomMapTable();
await db.transaction(async (trx) => {
const current = await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
.where({
shared_resource_token_id: normalizedTokenId,
session_id: normalizedSessionId,
})
.whereNull('archived_at')
.first();
const maxSortOrderRow = await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
.where({ shared_resource_token_id: normalizedTokenId })
.whereNull('archived_at')
.max<{ max_sort_order?: number | string | null }>('sort_order as max_sort_order')
.first();
const nextSortOrder = args.sortOrder != null
? Math.max(0, Math.trunc(Number(args.sortOrder) || 0))
: Math.max(0, normalizeInteger(maxSortOrderRow?.max_sort_order) + (current ? 0 : 1));
if (args.isDefault === true) {
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
.where({ shared_resource_token_id: normalizedTokenId })
.whereNull('archived_at')
.update({
is_default: false,
updated_at: db.fn.now(),
});
}
const payload = {
shared_resource_token_id: normalizedTokenId,
session_id: normalizedSessionId,
root_request_id: normalizedRootRequestId,
is_default: args.isDefault === true,
sort_order: nextSortOrder,
created_by_client_id: normalizeOptionalText(args.createdByClientId),
archived_at: null,
updated_at: db.fn.now(),
};
if (current) {
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
.where({
shared_resource_token_id: normalizedTokenId,
session_id: normalizedSessionId,
})
.update(payload);
return;
}
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE).insert({
...payload,
created_at: db.fn.now(),
});
});
return getChatShareTokenRoomMap(normalizedTokenId, normalizedSessionId);
}
export async function ensureDefaultChatShareTokenRoomMap(args: {
tokenId: string;
sessionId: string;
rootRequestId: string;
createdByClientId?: string | null;
}) {
const normalizedTokenId = args.tokenId.trim();
const normalizedSessionId = args.sessionId.trim();
const normalizedRootRequestId = args.rootRequestId.trim();
if (!normalizedTokenId || !normalizedSessionId || !normalizedRootRequestId) {
return [];
}
const existing = await getChatShareTokenRoomMap(normalizedTokenId, normalizedSessionId);
if (!existing) {
await upsertChatShareTokenRoomMap({
tokenId: normalizedTokenId,
sessionId: normalizedSessionId,
rootRequestId: normalizedRootRequestId,
isDefault: true,
createdByClientId: args.createdByClientId ?? null,
});
}
const rooms = await listChatShareTokenRoomMaps(normalizedTokenId);
if (rooms.some((item) => item.isDefault)) {
return rooms;
}
await upsertChatShareTokenRoomMap({
tokenId: normalizedTokenId,
sessionId: normalizedSessionId,
rootRequestId: normalizedRootRequestId,
isDefault: true,
createdByClientId: args.createdByClientId ?? null,
});
return listChatShareTokenRoomMaps(normalizedTokenId);
}
export async function resolveChatShareTokenRoomSessionIds(tokenId: string) {
const rooms = await listChatShareTokenRoomMaps(tokenId);
return rooms.map((item) => item.sessionId).filter(Boolean);
}