Описание изменений

This commit is contained in:
2025-03-19 17:18:03 +03:00
parent 2831527544
commit 87bad93eac
75 changed files with 2103 additions and 4861 deletions

View File

@@ -1,19 +1,21 @@
const createGuestMessagesTable = require('./migrations/create_guest_messages_table');
const { Pool } = require('pg');
const logger = require('../utils/logger');
async function initDatabase() {
try {
// ... существующий код ...
// Выполняем миграции
await pool.query(createUsersTable);
await pool.query(createSessionTable);
await pool.query(createNoncesTable);
await pool.query(createMessagesTable);
await pool.query(createConversationsTable);
await pool.query(createGuestMessagesTable);
// ... существующий код ...
} catch (error) {
// ... существующий код ...
const pool = new Pool({
user: process.env.DB_USER || 'dapp_user',
host: process.env.DB_HOST || 'localhost',
database: process.env.DB_NAME || 'dapp_db',
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT || 5432,
});
// Проверка подключения
pool.query('SELECT NOW()', (err, res) => {
if (err) {
logger.error('Error connecting to database:', err);
} else {
logger.info('Успешное подключение к базе данных:', res.rows[0]);
}
}
});
module.exports = { pool };

View File

@@ -1,8 +1,13 @@
const fs = require('fs');
const path = require('path');
const { pool } = require('./index');
const logger = require('../utils/logger');
// Инициализация таблицы roles
async function initRoles() {
try {
// Проверяем, существует ли таблица roles
const tableExists = await db.query(`
const tableExists = await pool.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'roles'
@@ -11,7 +16,7 @@ async function initRoles() {
if (!tableExists.rows[0].exists) {
// Создаем таблицу roles
await db.query(`
await pool.query(`
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
@@ -21,7 +26,7 @@ async function initRoles() {
`);
// Добавляем роли
await db.query(`
await pool.query(`
INSERT INTO roles (id, name, description) VALUES
(3, 'user', 'Обычный пользователь'),
(4, 'admin', 'Администратор с полным доступом');
@@ -30,24 +35,24 @@ async function initRoles() {
console.log('Таблица roles создана и заполнена');
} else {
// Проверяем наличие ролей
const rolesExist = await db.query(`
const rolesExist = await pool.query(`
SELECT COUNT(*) FROM roles WHERE id IN (3, 4);
`);
if (rolesExist.rows[0].count < 2) {
// Добавляем недостающие роли
const userRoleExists = await db.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'user');`);
const adminRoleExists = await db.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'admin');`);
const userRoleExists = await pool.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'user');`);
const adminRoleExists = await pool.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'admin');`);
if (!userRoleExists.rows[0].exists) {
await db.query(`
await pool.query(`
INSERT INTO roles (id, name, description) VALUES
(3, 'user', 'Обычный пользователь');
`);
}
if (!adminRoleExists.rows[0].exists) {
await db.query(`
await pool.query(`
INSERT INTO roles (id, name, description) VALUES
(4, 'admin', 'Администратор с полным доступом');
`);
@@ -60,4 +65,55 @@ async function initRoles() {
console.error('Ошибка при инициализации таблицы roles:', error);
throw error;
}
}
}
async function initializeDatabase() {
try {
// Создаем таблицу для отслеживания миграций, если её нет
await pool.query(`
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
executed_at TIMESTAMP NOT NULL DEFAULT NOW()
);
`);
// Путь к папке с миграциями
const migrationsPath = path.join(__dirname, 'migrations');
// Получаем все файлы миграций
const migrationFiles = fs.readdirSync(migrationsPath)
.filter(file => file.endsWith('.sql'))
.sort();
// Получаем выполненные миграции
const { rows } = await pool.query('SELECT name FROM migrations');
const executedMigrations = new Set(rows.map(row => row.name));
// Выполняем только новые миграции
for (const file of migrationFiles) {
if (!executedMigrations.has(file)) {
const filePath = path.join(migrationsPath, file);
const sql = fs.readFileSync(filePath, 'utf8');
logger.info(`Executing migration: ${file}`);
await pool.query(sql);
// Записываем выполненную миграцию
await pool.query(
'INSERT INTO migrations (name) VALUES ($1)',
[file]
);
logger.info(`Migration completed: ${file}`);
}
}
logger.info('All migrations completed successfully');
} catch (error) {
logger.error('Error during database initialization:', error);
throw error;
}
}
module.exports = { initializeDatabase };

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS "session" (
"sid" varchar NOT NULL COLLATE "default",
"sess" json NOT NULL,
"expire" timestamp(6) NOT NULL,
CONSTRAINT "session_pkey" PRIMARY KEY ("sid")
);
CREATE INDEX IF NOT EXISTS "IDX_session_expire" ON "session" ("expire");

View File

@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(255),
email VARCHAR(255) UNIQUE,
address VARCHAR(255) UNIQUE,
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Добавляем базовые роли
INSERT INTO roles (name) VALUES ('admin'), ('user')
ON CONFLICT (name) DO NOTHING;
-- Добавляем связь пользователей с ролями
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'role_id'
) THEN
ALTER TABLE users
ADD COLUMN role_id INTEGER REFERENCES roles(id);
END IF;
END $$;
-- Создаем индекс для role_id после добавления колонки
CREATE INDEX IF NOT EXISTS idx_users_role_id ON users(role_id);

View File

@@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS user_identities (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(provider, provider_id)
);
-- Создаем индексы после создания таблицы
DO $$
BEGIN
-- Индекс для user_id
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'user_identities' AND indexname = 'idx_user_identities_user_id'
) THEN
CREATE INDEX idx_user_identities_user_id ON user_identities(user_id);
END IF;
-- Индекс для provider и provider_id
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'user_identities' AND indexname = 'idx_user_identities_type_value'
) THEN
CREATE INDEX idx_user_identities_type_value ON user_identities(provider, provider_id);
END IF;
END $$;

View File

@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS conversations (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_conversations_user_id ON conversations(user_id);
CREATE INDEX IF NOT EXISTS idx_conversations_created_at ON conversations(created_at);

View File

@@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
conversation_id INTEGER REFERENCES conversations(id) ON DELETE CASCADE,
sender_type VARCHAR(20) NOT NULL,
sender_id INTEGER,
content TEXT,
channel VARCHAR(20) NOT NULL,
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
role VARCHAR(20) NOT NULL DEFAULT 'user',
guest_message_id INTEGER,
tokens_used INTEGER DEFAULT 0,
is_processed BOOLEAN DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
CREATE INDEX IF NOT EXISTS idx_messages_sender_type ON messages(sender_type);
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel);
CREATE INDEX IF NOT EXISTS idx_messages_metadata ON messages USING gin(metadata);

View File

@@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS guest_messages (
id SERIAL PRIMARY KEY,
guest_id VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
language VARCHAR(10) DEFAULT 'en',
is_ai BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'guest_messages' AND indexname = 'idx_guest_messages_guest_id'
) THEN
CREATE INDEX idx_guest_messages_guest_id ON guest_messages(guest_id);
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'guest_message_id'
) THEN
ALTER TABLE messages ADD COLUMN guest_message_id INTEGER;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_messages_guest_message'
) THEN
ALTER TABLE messages
ADD CONSTRAINT fk_messages_guest_message
FOREIGN KEY (guest_message_id)
REFERENCES guest_messages(id)
ON DELETE SET NULL;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'messages' AND indexname = 'idx_messages_guest_message_id'
) THEN
CREATE INDEX idx_messages_guest_message_id ON messages(guest_message_id);
END IF;
END $$;

View File

@@ -0,0 +1,46 @@
CREATE TABLE IF NOT EXISTS user_preferences (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
preference_key VARCHAR(50) NOT NULL,
preference_value TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(user_id, preference_key)
);
-- Добавляем колонку metadata, если её нет
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_preferences' AND column_name = 'metadata'
) THEN
ALTER TABLE user_preferences ADD COLUMN metadata JSONB DEFAULT '{}';
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);
-- Базовые настройки
DO $$
BEGIN
INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
SELECT id, 'language', 'ru', '{"available": ["ru", "en"]}'::jsonb
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM user_preferences
WHERE preference_key = 'language' AND user_id = u.id
);
END $$;
DO $$
BEGIN
INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
SELECT id, 'notifications', 'true', '{"channels": ["email", "telegram"]}'::jsonb
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM user_preferences
WHERE preference_key = 'notifications' AND user_id = u.id
);
END $$;

View File

@@ -0,0 +1,45 @@
-- Добавляем новые поля в messages
ALTER TABLE messages
ADD COLUMN IF NOT EXISTS role VARCHAR(20) NOT NULL DEFAULT 'user',
ADD COLUMN IF NOT EXISTS guest_message_id INTEGER,
ADD COLUMN IF NOT EXISTS tokens_used INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS is_processed BOOLEAN DEFAULT FALSE;
-- Создаем функцию для связывания сообщений
CREATE OR REPLACE FUNCTION link_guest_messages(
p_user_id INTEGER,
p_guest_id VARCHAR(255)
) RETURNS VOID AS $$
DECLARE
v_conversation_id INTEGER;
BEGIN
-- Создаем новую беседу для гостевых сообщений
INSERT INTO conversations (created_at, updated_at)
VALUES (NOW(), NOW())
RETURNING id INTO v_conversation_id;
-- Копируем гостевые сообщения в основную таблицу
INSERT INTO messages (
conversation_id,
sender_type,
sender_id,
content,
role,
channel,
guest_message_id,
created_at
)
SELECT
v_conversation_id,
CASE WHEN is_ai THEN 'assistant' ELSE 'user' END,
CASE WHEN NOT is_ai THEN p_user_id ELSE NULL END,
content,
CASE WHEN is_ai THEN 'assistant' ELSE 'user' END,
'chat',
id,
created_at
FROM guest_messages
WHERE guest_id = p_guest_id
ORDER BY created_at;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS nonces (
id SERIAL PRIMARY KEY,
identity_value VARCHAR(255) NOT NULL,
nonce VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Индекс для быстрого поиска по identity_value
CREATE INDEX IF NOT EXISTS idx_nonces_identity_value ON nonces(identity_value);
-- Индекс для очистки просроченных nonce
CREATE INDEX IF NOT EXISTS idx_nonces_expires_at ON nonces(expires_at);

View File

@@ -0,0 +1,71 @@
-- Проверяем существование типа user_role
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
CREATE TYPE user_role AS ENUM ('user', 'admin');
END IF;
END $$;
-- Удаляем лишние колонки и связи
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_role_id_fkey;
ALTER TABLE users DROP COLUMN IF EXISTS role_id;
-- Добавляем колонку role если её нет
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'role'
) THEN
ALTER TABLE users ADD COLUMN role user_role DEFAULT 'user'::user_role;
END IF;
END $$;
-- Удаляем лишние триггеры и функции
DROP TRIGGER IF EXISTS sync_identity_type_trigger ON user_identities;
DROP TRIGGER IF EXISTS user_identity_role_check ON user_identities;
DROP TRIGGER IF EXISTS check_admin_role_trigger ON user_identities;
DROP FUNCTION IF EXISTS sync_identity_type() CASCADE;
DROP FUNCTION IF EXISTS update_user_role() CASCADE;
DROP FUNCTION IF EXISTS check_admin_role(INTEGER) CASCADE;
-- Создаем функцию проверки роли
CREATE FUNCTION check_admin_role()
RETURNS TRIGGER AS $$
DECLARE
v_wallet_address VARCHAR;
BEGIN
SELECT provider_id INTO v_wallet_address
FROM user_identities
WHERE user_id = NEW.user_id
AND provider = 'wallet'
LIMIT 1;
IF v_wallet_address IS NULL THEN
RETURN NEW;
END IF;
UPDATE users
SET role = 'admin'::user_role
WHERE id = NEW.user_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Создаем триггер
CREATE TRIGGER check_admin_role_trigger
AFTER INSERT OR UPDATE ON user_identities
FOR EACH ROW
EXECUTE FUNCTION check_admin_role();
-- Обновляем существующие записи
UPDATE users u
SET role = CASE
WHEN EXISTS (
SELECT 1 FROM user_identities ui
WHERE ui.user_id = u.id
AND ui.provider = 'wallet'
) THEN 'admin'::user_role
ELSE 'user'::user_role
END;

View File

@@ -1,15 +0,0 @@
// Создаем таблицу для хранения сообщений неаутентифицированных пользователей
const createGuestMessagesTable = `
CREATE TABLE IF NOT EXISTS guest_messages (
id SERIAL PRIMARY KEY,
guest_id VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
language VARCHAR(10) DEFAULT 'en',
is_ai BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_guest_messages_guest_id ON guest_messages(guest_id);
`;
module.exports = createGuestMessagesTable;

View File

@@ -0,0 +1,56 @@
CREATE OR REPLACE FUNCTION link_guest_messages(
p_user_id INTEGER,
p_guest_id VARCHAR(255)
) RETURNS VOID AS $$
DECLARE
v_conversation_id INTEGER;
v_count INTEGER;
BEGIN
-- Логируем входные параметры
RAISE NOTICE 'Linking messages for user_id: %, guest_id: %', p_user_id, p_guest_id;
-- Проверяем наличие гостевых сообщений
SELECT COUNT(*) INTO v_count
FROM guest_messages
WHERE guest_id = p_guest_id;
RAISE NOTICE 'Found % guest messages', v_count;
-- Создаем новую беседу
INSERT INTO conversations (user_id, created_at, updated_at)
VALUES (p_user_id, NOW(), NOW())
RETURNING id INTO v_conversation_id;
RAISE NOTICE 'Created conversation with id: %', v_conversation_id;
-- Копируем сообщения пользователя
WITH inserted_messages AS (
INSERT INTO messages (
conversation_id,
sender_type,
sender_id,
content,
role,
channel,
guest_message_id,
created_at
)
SELECT
v_conversation_id,
CASE WHEN is_ai THEN 'assistant' ELSE 'user' END,
CASE WHEN NOT is_ai THEN p_user_id ELSE NULL END,
content,
CASE WHEN is_ai THEN 'assistant' ELSE 'user' END,
'chat',
id, -- Сохраняем связь с гостевым сообщением
created_at
FROM guest_messages
WHERE guest_id = p_guest_id
ORDER BY created_at
RETURNING id
)
SELECT COUNT(*) INTO v_count FROM inserted_messages;
RAISE NOTICE 'Inserted % messages', v_count;
END;
$$ LANGUAGE plpgsql;