# Servicio de Elasticsearch - SLM Framework API

Servicio completo de interacción con Elasticsearch usando Axios, equivalente al servicio PHP `slm_es_control.slm`.

## 📁 Ubicación

```
api/slm.framework/system/elasticsearch.slm
```

## 🚀 Características

- ✅ **Axios Instance Configurado**: Cliente HTTP con configuración base para ES
- ✅ **Autenticación Automática**: Credenciales desde variables de entorno
- ✅ **Interceptors**: Logging automático de requests/responses
- ✅ **Manejo de Errores**: Try-catch y logging estructurado
- ✅ **Funciones Completas**: CRUD + búsquedas avanzadas
- ✅ **Compatibilidad PHP**: Nombres de funciones similares para fácil migración

## 📚 Funciones Disponibles

### Configuración

```javascript
// Obtener configuración de ES
const server = slmapi.getEsServer();      // "http://elasticsearch:9200"
const index = slmapi.getEsIndex();        // "slm-app-dev"
const logIndex = slmapi.getEsIndexLog();  // "slm-app-dev-log"
const sessions = slmapi.getEsIndexSessions(); // "sessions"
```

### Operaciones CRUD

#### 1. Crear Objeto (setNewObject)

```javascript
const user = await slmapi.setNewObject({
    slm: {
        type: 'user'
    },
    name: 'John Doe',
    email: 'john@example.com',
    status: 'active'
}, 'user-123'); // ID opcional

console.log(user._id);     // "user-123"
console.log(user.result);  // "created"
```

**Características:**
- Agrega timestamp automáticamente si no existe
- ID opcional (ES genera uno si se omite)
- refresh=true para disponibilidad inmediata

#### 2. Obtener Objeto por ID (getObjectById)

```javascript
const user = await slmapi.getObjectById('user-123');

if (user) {
    console.log(user.name);   // "John Doe"
    console.log(user.email);  // "john@example.com"
} else {
    console.log('Usuario no encontrado');
}
```

**Retorna:**
- `Object` con `_source` si existe
- `null` si no existe

#### 3. Actualizar Objeto (updateObjectByID)

**Actualización simple:**
```javascript
await slmapi.updateObjectByID('user-123', {
    name: 'Jane Doe',
    lastUpdate: Date.now()
});
```

**Actualización con script:**
```javascript
await slmapi.updateObjectByID('user-123', {
    script: {
        source: 'ctx._source.counter += params.increment',
        params: { increment: 1 }
    }
});
```

#### 4. Eliminar Objeto (deleteObjectByID)

```javascript
const result = await slmapi.deleteObjectByID('user-123');
console.log(result.result); // "deleted"
```

### Búsquedas

#### 5. Buscar por Tipo (searchByType)

```javascript
// Buscar usuarios activos
const users = await slmapi.searchByType(
    'user',                           // tipo (slm.type)
    { 'status.keyword': 'active' },   // filtros
    10,                               // size
    0                                 // from (offset)
);

users.forEach(user => {
    console.log(user.id, user.name);
});
```

**Retorna:** Array de objetos con `id` incluido.

#### 6. Buscar con Query DSL (search)

```javascript
const results = await slmapi.search({
    query: {
        bool: {
            must: [
                { term: { 'slm.type': 'user' } },
                { match: { name: 'john' } }
            ],
            filter: [
                { range: { 'slm.timestamp': { gte: 'now-7d' } } }
            ]
        }
    },
    size: 10,
    from: 0,
    sort: [
        { 'slm.timestamp': { order: 'desc' } }
    ]
});

console.log('Total:', results.hits.total.value);
results.hits.hits.forEach(hit => {
    console.log(hit._id, hit._source);
});
```

#### 7. Obtener por Valor de Campo (getObjectWithValue)

```javascript
const user = await slmapi.getObjectWithValue(
    'user',                   // tipo
    'email.keyword',          // campo
    'john@example.com'        // valor
);

if (user.id) {
    console.log('Encontrado:', user.name);
}
```

**Retorna:** Objeto con `id` o `{}` si no existe.

### Operaciones Avanzadas

#### 8. Contar Documentos (countDocuments)

```javascript
const count = await slmapi.countDocuments({
    query: {
        bool: {
            must: [
                { term: { 'slm.type': 'user' } },
                { term: { 'status.keyword': 'active' } }
            ]
        }
    }
});

console.log('Usuarios activos:', count);
```

#### 9. Operaciones Bulk (bulkOperations)

```javascript
await slmapi.bulkOperations([
    { index: { _id: 'user-1' } },
    { slm: { type: 'user' }, name: 'User 1' },
    { index: { _id: 'user-2' } },
    { slm: { type: 'user' }, name: 'User 2' },
    { update: { _id: 'user-3' } },
    { doc: { status: 'inactive' } }
]);
```

#### 10. Verificar Existencia (existsDocument)

```javascript
const exists = await slmapi.existsDocument('user-123');
if (exists) {
    console.log('El documento existe');
}
```

#### 11. Ejecución Directa (es_execute)

Para operaciones personalizadas:

```javascript
// GET request
const response = await slmapi.es_execute('/_cluster/health', null, 'GET');

// POST request
const response = await slmapi.es_execute(
    '/slm-app-dev/_search',
    { query: { match_all: {} } },
    'POST'
);

// PUT request
const response = await slmapi.es_execute(
    '/slm-app-dev/_doc/123',
    { name: 'Test' },
    'PUT'
);

// DELETE request
const response = await slmapi.es_execute('/slm-app-dev/_doc/123', null, 'DELETE');
```

## 🔧 Configuración

### Variables de Entorno

En `api/.env`:

```env
# Elasticsearch - Desarrollo
ES_SERVER=http://elasticsearch:9200
DEV_ES_INDEX=slm-app-dev
DEV_ES_USERNAME=
DEV_ES_PASSWORD=

# Elasticsearch - Producción
ES_INDEX=slm-app-prod
ES_USERNAME=
ES_PASSWORD=
```

### Autenticación

Si defines `ES_USERNAME` y `ES_PASSWORD`, el servicio usará autenticación básica automáticamente:

```javascript
// Las credenciales se configuran automáticamente en axios.create()
const config = {
    baseURL: getEsServer(),
    auth: {
        username: process.env.ES_USERNAME,
        password: process.env.ES_PASSWORD
    }
};
```

## 📝 Ejemplos de Uso en Módulos

### Ejemplo 1: Crear y Actualizar Usuario

```javascript
module.exports = async (req, res, next, slmapi) => {
    const { name, email } = req.body;

    try {
        // Verificar si ya existe
        const existing = await slmapi.getObjectWithValue('user', 'email.keyword', email);
        if (existing.id) {
            return res.status(400).json({
                error: true,
                message: 'El email ya está registrado'
            });
        }

        // Crear usuario
        const result = await slmapi.setNewObject({
            slm: { type: 'user' },
            name,
            email,
            status: 'active',
            createdAt: slmapi.timestamp()
        });

        return res.status(201).json({
            error: false,
            data: {
                id: result._id,
                name,
                email
            }
        });
    } catch (error) {
        logger.error('Error creating user', { error: error.message });
        return res.status(500).json({
            error: true,
            message: 'Error al crear usuario'
        });
    }
};
```

### Ejemplo 2: Búsqueda con Paginación

```javascript
module.exports = async (req, res, next, slmapi) => {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const search = req.query.search || '';

    try {
        const from = (page - 1) * limit;

        // Construir query
        const query = {
            query: {
                bool: {
                    must: [
                        { term: { 'slm.type': 'user' } }
                    ]
                }
            },
            size: limit,
            from: from,
            sort: [{ 'slm.timestamp': { order: 'desc' } }]
        };

        // Agregar búsqueda si existe
        if (search) {
            query.query.bool.must.push({
                multi_match: {
                    query: search,
                    fields: ['name', 'email']
                }
            });
        }

        // Ejecutar búsqueda
        const results = await slmapi.search(query);

        // Contar total
        const total = await slmapi.countDocuments({ query: query.query });

        return res.status(200).json({
            error: false,
            data: {
                users: results.hits.hits.map(hit => ({
                    id: hit._id,
                    ...hit._source
                })),
                pagination: {
                    page,
                    limit,
                    total,
                    pages: Math.ceil(total / limit)
                }
            }
        });
    } catch (error) {
        logger.error('Error searching users', { error: error.message });
        return res.status(500).json({
            error: true,
            message: 'Error al buscar usuarios'
        });
    }
};
```

### Ejemplo 3: Actualizar con Validación

```javascript
module.exports = async (req, res, next, slmapi) => {
    const { id } = req.params;
    const updates = req.body;

    try {
        // Verificar que existe
        const exists = await slmapi.existsDocument(id);
        if (!exists) {
            return res.status(404).json({
                error: true,
                message: 'Usuario no encontrado'
            });
        }

        // Actualizar
        const result = await slmapi.updateObjectByID(id, {
            ...updates,
            lastUpdate: slmapi.timestamp()
        });

        // Obtener objeto actualizado
        const updated = await slmapi.getObjectById(id);

        return res.status(200).json({
            error: false,
            data: {
                id,
                ...updated
            }
        });
    } catch (error) {
        logger.error('Error updating user', { id, error: error.message });
        return res.status(500).json({
            error: true,
            message: 'Error al actualizar usuario'
        });
    }
};
```

## 🧪 Testing

### Probar el Demo de Elasticsearch

```bash
curl -H "X-API-Key: dev-key-12345" \
  http://localhost:3000/v1/example/es-demo
```

Este endpoint ejecuta todas las operaciones disponibles y retorna los resultados.

### Operaciones Individuales

```bash
# Crear documento
curl -X POST \
  -H "X-API-Key: dev-key-12345" \
  -H "Content-Type: application/json" \
  -d '{"slm":{"type":"test"},"name":"Test"}' \
  http://localhost:3000/v1/test/create

# Obtener documento
curl -H "X-API-Key: dev-key-12345" \
  http://localhost:3000/v1/test/test-123

# Buscar
curl -X POST \
  -H "X-API-Key: dev-key-12345" \
  -H "Content-Type: application/json" \
  -d '{"type":"test"}' \
  http://localhost:3000/v1/test/search
```

## 🔍 Logging

El servicio registra automáticamente todas las operaciones:

```javascript
// En desarrollo: logs en consola
// En producción: solo errores

// Logs generados:
// - ES Request: método, URL, datos
// - ES Response Error: status, mensaje
// - Operaciones exitosas: crear, actualizar, eliminar
```

Ver logs:
```bash
# Logs generales
cat api/logs/$(date +%Y-%m-%d).log

# Errores
cat api/error/$(date +%Y-%m-%d).log
```

## 🚨 Manejo de Errores

Todas las funciones lanzan errores que deben ser capturados:

```javascript
try {
    const result = await slmapi.getObjectById('123');
} catch (error) {
    if (error.response?.status === 404) {
        console.log('No encontrado');
    } else {
        console.error('Error de ES:', error.message);
    }
}
```

## 📊 Comparación con PHP

| Función PHP | Función Node.js | Notas |
|-------------|-----------------|-------|
| `setNewObject()` | `setNewObject()` | Idéntica |
| `updateObjectByID()` | `updateObjectByID()` | Idéntica |
| `getObjectById()` | `getObjectById()` | Idéntica |
| `getObjectWithValue()` | `getObjectWithValue()` | Idéntica |
| `es_execute()` | `es_execute()` | Idéntica |
| N/A | `deleteObjectByID()` | Nueva |
| N/A | `search()` | Nueva |
| N/A | `searchByType()` | Nueva |
| N/A | `countDocuments()` | Nueva |
| N/A | `bulkOperations()` | Nueva |
| N/A | `existsDocument()` | Nueva |

## 🎯 Mejores Prácticas

1. **Siempre usar try-catch**
   ```javascript
   try {
       const result = await slmapi.getObjectById(id);
   } catch (error) {
       logger.error('Error', { error: error.message });
   }
   ```

2. **Validar existencia antes de actualizar**
   ```javascript
   const exists = await slmapi.existsDocument(id);
   if (!exists) return res.status(404).json({...});
   ```

3. **Usar índices específicos cuando sea necesario**
   ```javascript
   await slmapi.setNewObject(data, id, 'custom-index');
   ```

4. **Aprovechar el logging automático**
   - El servicio registra todo automáticamente
   - Solo agregar logs de negocio específico

5. **Reutilizar queries complejas**
   ```javascript
   const activeUsersQuery = {
       query: {
           bool: {
               must: [
                   { term: { 'slm.type': 'user' } },
                   { term: { 'status.keyword': 'active' } }
               ]
           }
       }
   };
   ```

## 📚 Recursos

- [Elasticsearch Query DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html)
- [Axios Documentation](https://axios-http.com/docs/intro)
- [Servicio PHP Original](../appweb/slm.framework/system/slm_es_control.slm)

---

**Versión:** 1.0.0
**Última actualización:** Enero 2026
**Stack:** Node.js + Axios + Elasticsearch 7.9+
