feat: add Postgres MCP integration for database testing

- Add postgres-mcp service to docker-compose.yml (SSE mode on port 8000)
- Add .mcp.json.example with SSE configuration template
- Add .gitignore entries for .claude/settings.local.json and .mcp.json
- Add MCP_EXAMPLES.md with query examples for testing scraping results
- Add analysis scripts: analyze-category-nulls.ts, check-product-details.ts,
  inspect-api-response.ts

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-21 23:29:02 +05:00
parent 6ba22469c7
commit 5a763a4e13
8 changed files with 580 additions and 7 deletions

View File

@@ -0,0 +1,97 @@
import 'dotenv/config';
import { connectDatabase, disconnectDatabase, prisma } from '../config/database.js';
import { Logger } from '../utils/logger.js';
async function main() {
try {
await connectDatabase();
// Check total products and null categoryId count
const totalProducts = await prisma.product.count();
const nullCategoryCount = await prisma.product.count({
where: { categoryId: null }
});
const withCategoryCount = await prisma.product.count({
where: { categoryId: { not: null } }
});
Logger.info('\n📊 СТАТИСТИКА ПО КАТЕГОРИЯМ:');
Logger.info(`Всего товаров: ${totalProducts}`);
Logger.info(`Товаров без категории (null): ${nullCategoryCount} (${((nullCategoryCount / totalProducts) * 100).toFixed(2)}%)`);
Logger.info(`Товаров с категорией: ${withCategoryCount} (${((withCategoryCount / totalProducts) * 100).toFixed(2)}%)`);
// Check total categories
const totalCategories = await prisma.category.count();
Logger.info(`\nВсего категорий в БД: ${totalCategories}`);
// Sample categories
if (totalCategories > 0) {
const sampleCategories = await prisma.category.findMany({
take: 5,
select: {
id: true,
externalId: true,
name: true,
_count: {
select: { products: true }
}
}
});
Logger.info('\n📁 Примеры категорий:');
sampleCategories.forEach(cat => {
Logger.info(` - [${cat.externalId}] ${cat.name} (товаров: ${cat._count.products})`);
});
}
// Sample products without categories
const productsWithoutCategory = await prisma.product.findMany({
where: { categoryId: null },
take: 5,
select: {
id: true,
externalId: true,
name: true,
currentPrice: true
}
});
Logger.info('\n❌ Примеры товаров БЕЗ категории:');
productsWithoutCategory.forEach(p => {
Logger.info(` - [${p.externalId}] ${p.name} (₽${p.currentPrice})`);
});
// Sample products with categories
const productsWithCategory = await prisma.product.findMany({
where: { categoryId: { not: null } },
take: 5,
select: {
id: true,
externalId: true,
name: true,
currentPrice: true,
category: {
select: {
externalId: true,
name: true
}
}
}
});
if (productsWithCategory.length > 0) {
Logger.info('\n✅ Примеры товаров С категорией:');
productsWithCategory.forEach(p => {
Logger.info(` - [${p.externalId}] ${p.name} → [${p.category?.externalId}] ${p.category?.name}`);
});
}
} catch (error) {
Logger.error('❌ Ошибка при анализе:', error);
process.exit(1);
} finally {
await disconnectDatabase();
}
}
main();

View File

@@ -0,0 +1,38 @@
import 'dotenv/config';
import { connectDatabase, disconnectDatabase, prisma } from '../config/database.js';
import { Logger } from '../utils/logger.js';
async function main() {
try {
await connectDatabase();
// Get a sample product with all fields
const product = await prisma.product.findFirst({
select: {
id: true,
externalId: true,
name: true,
description: true,
currentPrice: true,
unit: true,
weight: true,
brand: true,
categoryId: true,
badges: true,
}
});
if (product) {
Logger.info('=== ДЕТАЛИ ТОВАРА ИЗ БД ===');
Logger.info(JSON.stringify(product, null, 2));
}
} catch (error) {
Logger.error('❌ Ошибка:', error);
process.exit(1);
} finally {
await disconnectDatabase();
}
}
main();

View File

@@ -0,0 +1,67 @@
import 'dotenv/config';
import { MagnitApiScraper } from '../scrapers/api/magnit/MagnitApiScraper.js';
import { Logger } from '../utils/logger.js';
async function main() {
const storeCode = process.env.MAGNIT_STORE_CODE || '992301';
const scraper = new MagnitApiScraper({
storeCode,
storeType: process.env.MAGNIT_STORE_TYPE || '6',
catalogType: process.env.MAGNIT_CATALOG_TYPE || '1',
headless: process.env.MAGNIT_HEADLESS !== 'false',
});
try {
await scraper.initialize();
Logger.info('Запрос первых 5 товаров для инспекции...\n');
const response = await scraper.searchGoods({ limit: 5, offset: 0 }, []);
Logger.info(`Получено товаров: ${response.items.length}\n`);
if (response.items.length > 0) {
Logger.info('=== СТРУКТУРА ПЕРВОГО ТОВАРА ===');
const firstProduct = response.items[0];
Logger.info(JSON.stringify(firstProduct, null, 2));
Logger.info('\n=== ПРОВЕРКА НАЛИЧИЯ КАТЕГОРИЙ ===');
response.items.forEach((item, index) => {
Logger.info(
`${index + 1}. [${item.id}] ${item.name.substring(0, 50)}...`
);
if (item.category) {
Logger.info(` ✅ Категория: [${item.category.id}] ${item.category.title}`);
} else {
Logger.info(` ❌ Категория отсутствует (undefined)`);
}
});
Logger.info('\n=== ОТВЕТ API (response.category) ===');
if (response.category) {
Logger.info(`Категория уровня ответа: [${response.category.id}] ${response.category.title}`);
} else {
Logger.info('Категория уровня ответа отсутствует');
}
Logger.info('\n=== БЫСТРЫЕ КАТЕГОРИИ (fastCategoriesExtended) ===');
if (response.fastCategoriesExtended && response.fastCategoriesExtended.length > 0) {
Logger.info(`Найдено ${response.fastCategoriesExtended.length} быстрых категорий:`);
response.fastCategoriesExtended.slice(0, 10).forEach(cat => {
Logger.info(` - [${cat.id}] ${cat.title}`);
});
} else {
Logger.info('Быстрые категории отсутствуют');
}
}
} catch (error) {
Logger.error('❌ Ошибка:', error);
process.exit(1);
} finally {
await scraper.close();
}
}
main();