Compare commits

...

10 Commits

Author SHA1 Message Date
9a08396610 new file: .dockerignore
new file:   .env.example
	new file:   Dockerfile
	modified:   backend/.env.example
	modified:   backend/package.json
	renamed:    backend/src/migrations/run.ts -> backend/src/scripts/run.ts
	renamed:    backend/src/migrations/seed.ts -> backend/src/scripts/seed.ts
	new file:   docker-compose.external-db.yml
	new file:   docker-compose.yml
2025-12-18 13:01:25 +03:00
GEgorov
12736f5b79 modified: .gitignore 2025-12-18 12:37:55 +03:00
GEgorov
5255e0622e modified: frontend/src/components/EndpointModal.tsx 2025-12-15 16:20:39 +03:00
GEgorov
afd79b9c2e modified: frontend/src/components/EndpointModal.tsx 2025-12-15 16:18:51 +03:00
GEgorov
bd755cd19f modified: backend/src/services/ScriptExecutor.ts 2025-12-15 16:15:59 +03:00
GEgorov
0cca6f5d8e deleted: backend/check_endpoints.js
modified:   backend/src/services/SqlExecutor.ts
2025-11-29 16:37:31 +03:00
GEgorov
675d455d23 modified: backend/src/services/SqlExecutor.ts 2025-11-29 16:28:02 +03:00
GEgorov
09ca6e1fd2 modified: backend/src/services/SqlExecutor.ts 2025-11-29 16:21:29 +03:00
GEgorov
ced086db7f modified: backend/src/services/SqlExecutor.ts 2025-11-29 16:18:39 +03:00
GEgorov
31506d2f87 modified: backend/src/services/SqlExecutor.ts 2025-11-29 16:14:54 +03:00
14 changed files with 437 additions and 249 deletions

44
.dockerignore Normal file
View File

@@ -0,0 +1,44 @@
# Dependencies
node_modules
**/node_modules
# Build outputs (will be rebuilt in container)
dist
**/dist
build
**/build
# Git
.git
.gitignore
# IDE
.idea
.vscode
*.swp
*.swo
# Logs
*.log
logs
# Environment files
.env
.env.local
.env.*.local
# OS files
.DS_Store
Thumbs.db
# Test files
coverage
.nyc_output
# Project-specific (not needed in app container)
.claude
.git_backup
db_connections
final_endpoints_v2
nowContext
queries

29
.env.example Normal file
View File

@@ -0,0 +1,29 @@
# ============================================
# KIS API Builder - Configuration
# ============================================
# Copy this file to .env and adjust values
#
# For default setup (built-in DB):
# Only APP_PORT, DB_PASSWORD and JWT_SECRET are needed
#
# For external database:
# Set all DB_* variables
# ============================================
# External port (access from host machine)
APP_PORT=3000
# Database password (used by both built-in and external DB)
DB_PASSWORD=your_secure_password_here
# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRES_IN=24h
# ============================================
# External Database (only for docker-compose.external-db.yml)
# ============================================
# DB_HOST=your-postgres-host
# DB_PORT=5432
# DB_NAME=api_builder
# DB_USER=postgres

3
.gitignore vendored
View File

@@ -11,4 +11,5 @@ coverage/
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.claude .claude
.git_backup/

68
Dockerfile Normal file
View File

@@ -0,0 +1,68 @@
# ============================================
# Stage 1: Build Frontend
# ============================================
FROM node:20-alpine AS frontend-builder
WORKDIR /app/frontend
# Copy frontend package files
COPY frontend/package*.json ./
# Install dependencies
RUN npm ci
# Copy frontend source
COPY frontend/ ./
# Build frontend
RUN npm run build
# ============================================
# Stage 2: Build Backend
# ============================================
FROM node:20-alpine AS backend-builder
WORKDIR /app/backend
# Copy backend package files
COPY backend/package*.json ./
# Install dependencies
RUN npm ci
# Copy backend source
COPY backend/ ./
# Build TypeScript
RUN npm run build
# ============================================
# Stage 3: Production
# ============================================
FROM node:20-alpine AS production
WORKDIR /app
# Copy backend package files and install production deps
COPY backend/package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy built backend
COPY --from=backend-builder /app/backend/dist ./dist
# Copy built frontend to the location expected by backend
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# Start the application
CMD ["node", "dist/server.js"]

View File

@@ -15,31 +15,4 @@ JWT_EXPIRES_IN=24h
# API Rate Limiting # API Rate Limiting
RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100 RATE_LIMIT_MAX_REQUESTS=100
# Target Databases Configuration (JSON format)
# This is where your API endpoints will execute queries
TARGET_DATABASES='[
{
"id": "main_postgres",
"name": "Main PostgreSQL",
"type": "postgresql",
"host": "localhost",
"port": 5432,
"database": "your_database",
"user": "your_user",
"password": "your_password",
"ssl": false
},
{
"id": "analytics_db",
"name": "Analytics Database",
"type": "postgresql",
"host": "localhost",
"port": 5432,
"database": "analytics",
"user": "analytics_user",
"password": "analytics_password",
"ssl": false
}
]'

View File

@@ -1,49 +0,0 @@
const { Client } = require('pg');
async function testQueries() {
// Подключаемся к целевой БД (emias_pg)
const client = new Client({
host: 'm112-pgkisc-01.ncms-i.ru',
port: 5432,
database: 'kis',
user: 'XАПИД',
password: 'c4d504412b61b0560d442686dfad27'
});
await client.connect();
console.log('Connected to kis database');
const caseId = 'f580b03b-86ee-41b6-a697-1981f116c669';
// Запрос из проблемного эндпоинта (с табами)
const queryWithTabs = `SELECT\tea.c_uid a_uid,
\tp.ehr_id ehrid,
\tp.erz_number subjectid,
\tp.namespace namespace
FROM \tmm.ehr_case ec
\tINNER JOIN mm.ehr_case_action ea ON ec.last_action_id = ea.id
\tINNER JOIN mm.hospdoc hd ON hd.ehr_case_id = ec.id
\tINNER JOIN mm.mdoc md ON md.id = hd.mdoc_id
\tINNER JOIN mm.people p ON p.id = md.people_id
WHERE\tec.id = $1
AND\thd.location_status_id = 1`;
// Запрос из рабочего эндпоинта (с пробелами и CRLF)
const queryWithSpaces = `SELECT ea.c_uid a_uid,\r\n p.ehr_id ehrid,\r\n p.erz_number subjectid,\r\n p.namespace namespace\r\n FROM mm.ehr_case ec\r\n INNER JOIN mm.ehr_case_action ea ON ec.last_action_id = ea.id\r\n INNER JOIN mm.hospdoc hd ON hd.ehr_case_id = ec.id\r\n INNER JOIN mm.mdoc md ON md.id = hd.mdoc_id\r\n INNER JOIN mm.people p ON p.id = md.people_id\r\n WHERE ec.id = $1\r\n AND hd.location_status_id = 1`;
console.log('\n=== Query with TABS (problematic) ===');
console.log('HEX first 50:', Buffer.from(queryWithTabs.substring(0, 50)).toString('hex'));
const result1 = await client.query(queryWithTabs, [caseId]);
console.log('rowCount:', result1.rowCount);
console.log('rows:', JSON.stringify(result1.rows));
console.log('\n=== Query with SPACES (working) ===');
console.log('HEX first 50:', Buffer.from(queryWithSpaces.substring(0, 50)).toString('hex'));
const result2 = await client.query(queryWithSpaces, [caseId]);
console.log('rowCount:', result2.rowCount);
console.log('rows:', JSON.stringify(result2.rows));
await client.end();
}
testQueries().catch(console.error);

View File

@@ -7,8 +7,8 @@
"dev": "nodemon", "dev": "nodemon",
"build": "tsc", "build": "tsc",
"start": "node dist/server.js", "start": "node dist/server.js",
"migrate": "ts-node src/migrations/run.ts", "migrate": "ts-node src/scripts/run.ts",
"seed": "ts-node src/migrations/seed.ts" "seed": "ts-node src/scripts/seed.ts"
}, },
"keywords": [ "keywords": [
"api", "api",

View File

@@ -6,14 +6,18 @@ async function runMigrations() {
console.log('Running migrations...'); console.log('Running migrations...');
try { try {
const migrationFile = fs.readFileSync( const migrationsDir = path.join(__dirname, '../migrations');
path.join(__dirname, '001_initial_schema.sql'), const files = fs.readdirSync(migrationsDir)
'utf-8' .filter(f => f.endsWith('.sql'))
); .sort();
for (const file of files) {
console.log(` Running ${file}...`);
const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf-8');
await mainPool.query(sql);
}
await mainPool.query(migrationFile);
console.log('✅ Migrations completed successfully'); console.log('✅ Migrations completed successfully');
process.exit(0); process.exit(0);
} catch (error) { } catch (error) {
console.error('❌ Migration failed:', error); console.error('❌ Migration failed:', error);

View File

@@ -108,9 +108,9 @@ export class ScriptExecutor {
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
const userFunction = new AsyncFunction('params', 'execQuery', code); const userFunction = new AsyncFunction('params', 'execQuery', code);
// Устанавливаем таймаут // Устанавливаем таймаут (10 минут)
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Script execution timeout (30s)')), 30000); setTimeout(() => reject(new Error('Script execution timeout (10min)')), 600000);
}); });
// Выполняем скрипт с таймаутом // Выполняем скрипт с таймаутом
@@ -322,11 +322,11 @@ print(json.dumps(result))
} }
}); });
// Таймаут 30 секунд // Таймаут 10 минут
setTimeout(() => { setTimeout(() => {
python.kill(); python.kill();
reject(new Error('Python script execution timeout (30s)')); reject(new Error('Python script execution timeout (10min)'));
}, 30000); }, 600000);
}); });
} }

View File

@@ -47,18 +47,12 @@ export class SqlExecutor {
this.validateQuery(sqlQuery); this.validateQuery(sqlQuery);
// Log SQL query and parameters before execution // Log SQL query and parameters before execution
console.log('\n[SQL DB]', databaseId); console.log('\n[SQL Query]', sqlQuery);
// @ts-ignore - accessing pool options for debugging
const poolOpts = pool.options;
console.log('[SQL Pool Config] host:', poolOpts?.host, 'database:', poolOpts?.database, 'user:', poolOpts?.user);
console.log('[SQL Query]', sqlQuery);
console.log('[SQL Params]', params); console.log('[SQL Params]', params);
// Execute with retry mechanism // Execute with retry mechanism
const result = await this.retryQuery(async () => { const result = await this.retryQuery(async () => {
const queryResult = await pool.query(sqlQuery, params); return await pool.query(sqlQuery, params);
console.log('[SQL Result] rowCount:', queryResult.rowCount, 'rows:', JSON.stringify(queryResult.rows).substring(0, 500));
return queryResult;
}, 3, 500); // 3 попытки с задержкой 500ms }, 3, 500); // 3 попытки с задержкой 500ms
const executionTime = Date.now() - startTime; const executionTime = Date.now() - startTime;

View File

@@ -0,0 +1,29 @@
# ============================================
# KIS API Builder - External Database
# ============================================
# Use this when you have your own PostgreSQL
#
# 1. Copy .env.example to .env
# 2. Set DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
# 3. Run: docker compose -f docker-compose.external-db.yml up -d
# ============================================
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: kis-api-builder-app
restart: unless-stopped
ports:
- "${APP_PORT:-3000}:3000"
environment:
NODE_ENV: production
PORT: 3000
DB_HOST: ${DB_HOST:?DB_HOST is required}
DB_PORT: ${DB_PORT:-5432}
DB_NAME: ${DB_NAME:-api_builder}
DB_USER: ${DB_USER:-postgres}
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
JWT_SECRET: ${JWT_SECRET:-change-this-secret-in-production}
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-24h}

63
docker-compose.yml Normal file
View File

@@ -0,0 +1,63 @@
# ============================================
# KIS API Builder - Docker Compose
# ============================================
# Default setup with built-in PostgreSQL
# Just run: docker compose up -d
#
# For external database, use:
# docker compose -f docker-compose.external-db.yml up -d
# ============================================
services:
# PostgreSQL Database (built-in)
db:
image: postgres:16-alpine
container_name: kis-api-builder-db
restart: unless-stopped
environment:
POSTGRES_DB: api_builder
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backend/src/migrations:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d api_builder"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
networks:
- kis-network
# Application (Backend + Frontend)
app:
build:
context: .
dockerfile: Dockerfile
container_name: kis-api-builder-app
restart: unless-stopped
ports:
- "${APP_PORT:-3000}:3000"
environment:
NODE_ENV: production
PORT: 3000
DB_HOST: db
DB_PORT: 5432
DB_NAME: api_builder
DB_USER: postgres
DB_PASSWORD: ${DB_PASSWORD:-postgres}
JWT_SECRET: ${JWT_SECRET:-change-this-secret-in-production}
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-24h}
depends_on:
db:
condition: service_healthy
networks:
- kis-network
volumes:
postgres_data:
networks:
kis-network:
driver: bridge

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'; import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
import { endpointsApi, foldersApi } from '@/services/api'; import { endpointsApi, foldersApi } from '@/services/api';
import { Endpoint, EndpointParameter } from '@/types'; import { Endpoint, EndpointParameter } from '@/types';
import { Plus, Trash2, Play, Edit2 } from 'lucide-react'; import { Plus, Trash2, Play, Edit2, ChevronDown, ChevronUp } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import SqlEditor from '@/components/SqlEditor'; import SqlEditor from '@/components/SqlEditor';
import CodeEditor from '@/components/CodeEditor'; import CodeEditor from '@/components/CodeEditor';
@@ -45,6 +45,8 @@ export default function EndpointModal({
const [editingQueryIndex, setEditingQueryIndex] = useState<number | null>(null); const [editingQueryIndex, setEditingQueryIndex] = useState<number | null>(null);
const [showScriptCodeEditor, setShowScriptCodeEditor] = useState(false); const [showScriptCodeEditor, setShowScriptCodeEditor] = useState(false);
const [parametersExpanded, setParametersExpanded] = useState(true);
const [queriesExpanded, setQueriesExpanded] = useState(true);
// Определяем тип выбранной базы данных // Определяем тип выбранной базы данных
const selectedDatabase = databases.find(db => db.id === formData.database_id); const selectedDatabase = databases.find(db => db.id === formData.database_id);
@@ -227,17 +229,29 @@ export default function EndpointModal({
</div> </div>
</div> </div>
<div> <div className="border border-gray-200 rounded-lg">
<div className="flex items-center justify-between mb-2"> <div
<label className="block text-sm font-medium text-gray-700"> className="flex items-center justify-between p-3 bg-gray-50 cursor-pointer hover:bg-gray-100 rounded-t-lg"
Параметры запроса onClick={() => setParametersExpanded(!parametersExpanded)}
<span className="text-xs text-gray-500 ml-2"> >
<div className="flex items-center gap-2">
{parametersExpanded ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
<label className="text-sm font-medium text-gray-700 cursor-pointer">
Параметры запроса
{formData.parameters.length > 0 && (
<span className="ml-2 px-2 py-0.5 bg-primary-100 text-primary-700 rounded-full text-xs">
{formData.parameters.length}
</span>
)}
</label>
<span className="text-xs text-gray-500">
(используйте $имяПараметра в QL запросе) (используйте $имяПараметра в QL запросе)
</span> </span>
</label> </div>
<button <button
type="button" type="button"
onClick={() => { onClick={(e) => {
e.stopPropagation();
const newParam: EndpointParameter = { const newParam: EndpointParameter = {
name: '', name: '',
type: 'string' as const, type: 'string' as const,
@@ -246,100 +260,103 @@ export default function EndpointModal({
description: '', description: '',
}; };
setFormData({ ...formData, parameters: [...formData.parameters, newParam] }); setFormData({ ...formData, parameters: [...formData.parameters, newParam] });
setParametersExpanded(true);
}} }}
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1" className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
> >
<Plus size={16} /> <Plus size={16} />
Добавить параметр Добавить
</button> </button>
</div> </div>
{formData.parameters.length > 0 ? ( {parametersExpanded && (
<div className="space-y-3 mb-4 border border-gray-200 rounded-lg p-4"> formData.parameters.length > 0 ? (
{formData.parameters.map((param: any, index: number) => ( <div className="space-y-3 p-4">
<div key={index} className="flex gap-2 items-start bg-gray-50 p-3 rounded"> {formData.parameters.map((param: any, index: number) => (
<div className="flex-1 grid grid-cols-5 gap-2"> <div key={index} className="flex gap-2 items-start bg-gray-50 p-3 rounded">
<input <div className="flex-1 grid grid-cols-5 gap-2">
type="text"
placeholder="Имя параметра"
value={param.name}
onChange={(e) => {
const newParams = [...formData.parameters];
newParams[index].name = e.target.value;
setFormData({ ...formData, parameters: newParams });
}}
className="input text-sm"
/>
<select
value={param.type}
onChange={(e) => {
const newParams = [...formData.parameters];
newParams[index].type = e.target.value as 'string' | 'number' | 'boolean' | 'date';
setFormData({ ...formData, parameters: newParams });
}}
className="input text-sm"
>
<option value="string">string</option>
<option value="number">number</option>
<option value="boolean">boolean</option>
<option value="date">date</option>
</select>
<select
value={param.in}
onChange={(e) => {
const newParams = [...formData.parameters];
newParams[index].in = e.target.value as 'query' | 'body' | 'path';
setFormData({ ...formData, parameters: newParams });
}}
className="input text-sm"
>
<option value="query">Query</option>
<option value="body">Body</option>
<option value="path">Path</option>
</select>
<input
type="text"
placeholder="Описание"
value={param.description || ''}
onChange={(e) => {
const newParams = [...formData.parameters];
newParams[index].description = e.target.value;
setFormData({ ...formData, parameters: newParams });
}}
className="input text-sm"
/>
<label className="flex items-center gap-1 text-sm">
<input <input
type="checkbox" type="text"
checked={param.required} placeholder="Имя параметра"
value={param.name}
onChange={(e) => { onChange={(e) => {
const newParams = [...formData.parameters]; const newParams = [...formData.parameters];
newParams[index].required = e.target.checked; newParams[index].name = e.target.value;
setFormData({ ...formData, parameters: newParams }); setFormData({ ...formData, parameters: newParams });
}} }}
className="rounded" className="input text-sm"
/> />
<span className="text-xs">Обязательный</span> <select
</label> value={param.type}
onChange={(e) => {
const newParams = [...formData.parameters];
newParams[index].type = e.target.value as 'string' | 'number' | 'boolean' | 'date';
setFormData({ ...formData, parameters: newParams });
}}
className="input text-sm"
>
<option value="string">string</option>
<option value="number">number</option>
<option value="boolean">boolean</option>
<option value="date">date</option>
</select>
<select
value={param.in}
onChange={(e) => {
const newParams = [...formData.parameters];
newParams[index].in = e.target.value as 'query' | 'body' | 'path';
setFormData({ ...formData, parameters: newParams });
}}
className="input text-sm"
>
<option value="query">Query</option>
<option value="body">Body</option>
<option value="path">Path</option>
</select>
<input
type="text"
placeholder="Описание"
value={param.description || ''}
onChange={(e) => {
const newParams = [...formData.parameters];
newParams[index].description = e.target.value;
setFormData({ ...formData, parameters: newParams });
}}
className="input text-sm"
/>
<label className="flex items-center gap-1 text-sm">
<input
type="checkbox"
checked={param.required}
onChange={(e) => {
const newParams = [...formData.parameters];
newParams[index].required = e.target.checked;
setFormData({ ...formData, parameters: newParams });
}}
className="rounded"
/>
<span className="text-xs">Обязательный</span>
</label>
</div>
<button
type="button"
onClick={() => {
const newParams = formData.parameters.filter((_: any, i: number) => i !== index);
setFormData({ ...formData, parameters: newParams });
}}
className="p-1 hover:bg-red-50 rounded text-red-600"
title="Удалить параметр"
>
<Trash2 size={16} />
</button>
</div> </div>
<button ))}
type="button" </div>
onClick={() => { ) : (
const newParams = formData.parameters.filter((_: any, i: number) => i !== index); <div className="text-center py-4 bg-white rounded-b-lg">
setFormData({ ...formData, parameters: newParams }); <p className="text-sm text-gray-500">Нет параметров. Добавьте параметры для динамического запроса.</p>
}} </div>
className="p-1 hover:bg-red-50 rounded text-red-600" )
title="Удалить параметр"
>
<Trash2 size={16} />
</button>
</div>
))}
</div>
) : (
<div className="text-center py-4 mb-4 border border-gray-200 rounded-lg bg-gray-50">
<p className="text-sm text-gray-500">Нет параметров. Добавьте параметры для динамического запроса.</p>
</div>
)} )}
</div> </div>
@@ -447,14 +464,26 @@ export default function EndpointModal({
</select> </select>
</div> </div>
<div> <div className="border border-gray-200 rounded-lg">
<div className="flex items-center justify-between mb-2"> <div
<label className="block text-sm font-medium text-gray-700"> className="flex items-center justify-between p-3 bg-gray-50 cursor-pointer hover:bg-gray-100 rounded-t-lg"
SQL Запросы для скрипта onClick={() => setQueriesExpanded(!queriesExpanded)}
</label> >
<div className="flex items-center gap-2">
{queriesExpanded ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
<label className="text-sm font-medium text-gray-700 cursor-pointer">
SQL Запросы для скрипта
{formData.script_queries.length > 0 && (
<span className="ml-2 px-2 py-0.5 bg-primary-100 text-primary-700 rounded-full text-xs">
{formData.script_queries.length}
</span>
)}
</label>
</div>
<button <button
type="button" type="button"
onClick={() => { onClick={(e) => {
e.stopPropagation();
const newQueries = [...formData.script_queries, { const newQueries = [...formData.script_queries, {
name: '', name: '',
sql: '', sql: '',
@@ -466,72 +495,75 @@ export default function EndpointModal({
}]; }];
setFormData({ ...formData, script_queries: newQueries }); setFormData({ ...formData, script_queries: newQueries });
setEditingQueryIndex(newQueries.length - 1); setEditingQueryIndex(newQueries.length - 1);
setQueriesExpanded(true);
}} }}
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1" className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
> >
<Plus size={16} /> <Plus size={16} />
Добавить запрос Добавить
</button> </button>
</div> </div>
{formData.script_queries.length > 0 ? ( {queriesExpanded && (
<div className="space-y-2 mb-4"> formData.script_queries.length > 0 ? (
{formData.script_queries.map((query: any, idx: number) => ( <div className="space-y-2 p-4">
<div key={idx} className="border border-gray-200 rounded-lg p-4 bg-white hover:shadow-sm transition-shadow"> {formData.script_queries.map((query: any, idx: number) => (
<div className="flex items-center justify-between"> <div key={idx} className="border border-gray-200 rounded-lg p-4 bg-white hover:shadow-sm transition-shadow">
<div className="flex-1"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2 mb-1"> <div className="flex-1">
<code className="text-sm font-semibold text-gray-900">{query.name || 'Безымянный запрос'}</code> <div className="flex items-center gap-2 mb-1">
{query.database_id && ( <code className="text-sm font-semibold text-gray-900">{query.name || 'Безымянный запрос'}</code>
<> {query.database_id && (
<span className="text-xs text-gray-500"> <>
{databases.find(db => db.id === query.database_id)?.name || 'БД не выбрана'} <span className="text-xs text-gray-500">
</span> {databases.find(db => db.id === query.database_id)?.name || 'БД не выбрана'}
{databases.find(db => db.id === query.database_id)?.type === 'aql' && ( </span>
<span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded">AQL</span> {databases.find(db => db.id === query.database_id)?.type === 'aql' && (
)} <span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded">AQL</span>
</> )}
</>
)}
</div>
{query.sql && (
<div className="text-xs text-gray-600 font-mono bg-gray-50 p-2 rounded mt-1 truncate">
{query.sql.substring(0, 100)}{query.sql.length > 100 ? '...' : ''}
</div>
)}
{query.aql_endpoint && (
<div className="text-xs text-gray-600 font-mono bg-purple-50 p-2 rounded mt-1">
<span className="text-purple-700 font-semibold">{query.aql_method}</span> {query.aql_endpoint}
</div>
)} )}
</div> </div>
{query.sql && ( <div className="flex gap-2 ml-4">
<div className="text-xs text-gray-600 font-mono bg-gray-50 p-2 rounded mt-1 truncate"> <button
{query.sql.substring(0, 100)}{query.sql.length > 100 ? '...' : ''} type="button"
</div> onClick={() => setEditingQueryIndex(idx)}
)} className="p-2 hover:bg-blue-50 rounded text-blue-600"
{query.aql_endpoint && ( title="Редактировать запрос"
<div className="text-xs text-gray-600 font-mono bg-purple-50 p-2 rounded mt-1"> >
<span className="text-purple-700 font-semibold">{query.aql_method}</span> {query.aql_endpoint} <Edit2 size={16} />
</div> </button>
)} <button
</div> type="button"
<div className="flex gap-2 ml-4"> onClick={() => {
<button const newQueries = formData.script_queries.filter((_: any, i: number) => i !== idx);
type="button" setFormData({ ...formData, script_queries: newQueries });
onClick={() => setEditingQueryIndex(idx)} }}
className="p-2 hover:bg-blue-50 rounded text-blue-600" className="p-2 hover:bg-red-50 rounded text-red-600"
title="Редактировать запрос" title="Удалить запрос"
> >
<Edit2 size={16} /> <Trash2 size={16} />
</button> </button>
<button </div>
type="button"
onClick={() => {
const newQueries = formData.script_queries.filter((_: any, i: number) => i !== idx);
setFormData({ ...formData, script_queries: newQueries });
}}
className="p-2 hover:bg-red-50 rounded text-red-600"
title="Удалить запрос"
>
<Trash2 size={16} />
</button>
</div> </div>
</div> </div>
</div> ))}
))} </div>
</div> ) : (
) : ( <div className="text-center py-6 bg-white rounded-b-lg">
<div className="text-center py-6 mb-4 border border-gray-200 rounded-lg bg-gray-50"> <p className="text-sm text-gray-500">Нет SQL запросов. Добавьте запросы для использования в скрипте.</p>
<p className="text-sm text-gray-500">Нет SQL запросов. Добавьте запросы для использования в скрипте.</p> </div>
</div> )
)} )}
</div> </div>