chore: test deploy snapshot
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user