npm.io
2.10.0 • Published 2h ago

@npmtapi/tapi-lib-dynamo-db

Licence
ISC
Version
2.10.0
Deps
1
Size
102 kB
Vulns
0
Weekly
0

TAPI DynamoDB Library

Una librería Node.js moderna para interactuar con Amazon DynamoDB de manera sencilla y eficiente, diseñada para usar ES Modules (ESM).

Características

  • Compatible con ES Modules (ESM)
  • Soporte completo para operaciones CRUD
  • Operaciones de consulta y escaneo avanzadas
  • Operaciones en lote (batch operations)
  • Soporte para índices globales y locales (GSI/LSI)
  • Manejo automático de paginación
  • Creación condicional de elementos
  • TypeScript friendly con JSDoc completo

Instalación

npm install @tapila/tapi-lib-dynamo-db

Configuración

Configuración básica con AWS
import { dynamoConnection, DynamoModel } from '@tapila/tapi-lib-dynamo-db';

// Configurar el cliente de DynamoDB usando la función de conexión de la librería
const region = 'us-east-1';
const client = dynamoConnection(region);

// Crear una instancia del modelo
const userModel = new DynamoModel('users-table', client);
Configuración para DynamoDB Local
import { dynamoConnection, DynamoModel } from '@tapila/tapi-lib-dynamo-db';

// Configuración para DynamoDB Local
const region = 'us-east-1';
const dynamoEndpoint = 'http://localhost:8000';
const client = dynamoConnection(region, dynamoEndpoint);

// Crear una instancia del modelo
const userModel = new DynamoModel('users-table', client);
Configuración avanzada con variables de entorno
import { dynamoConnection, DynamoModel } from '@tapila/tapi-lib-dynamo-db';

// Usar variables de entorno para configuración flexible
const region = process.env.AWS_REGION || 'us-east-1';
const dynamoEndpoint = process.env.DYNAMO_ENDPOINT; // undefined para AWS, URL para local

const client = dynamoConnection(region, dynamoEndpoint);
const userModel = new DynamoModel(process.env.TABLE_NAME || 'users-table', client);

Uso y Ejemplos

findOne - Buscar un elemento por clave primaria
// Buscar por clave simple
const user = await userModel.findOne({
  key: { id: 'user-123' }
});

// Buscar por clave compuesta (partition key + sort key)
const userProfile = await userModel.findOne({
  key: { 
    PK: 'user-123', 
    SK: 'profile' 
  }
});

console.log(user); // { id: 'user-123', name: 'John Doe', email: 'john@example.com' } o null
create - Crear un nuevo elemento
// Crear un nuevo usuario
const newUser = await userModel.create({
  id: 'user-123',
  name: 'John Doe',
  email: 'john@example.com',
  createdAt: new Date().toISOString(),
  status: 'active'
});

// Crear con opciones personalizadas
const newUserWithOptions = await userModel.create({
  id: 'user-124',
  name: 'Jane Smith',
  email: 'jane@example.com',
  tempField: undefined // Se eliminará automáticamente
}, {
  removeUndefinedValues: true
});

console.log(newUser); // null si se creó exitosamente, o el elemento anterior si ya existía
createIfNotExists - Crear solo si no existe
try {
  const result = await userModel.createIfNotExists({
    id: 'user-125',
    name: 'Bob Johnson',
    email: 'bob@example.com',
    uniqueCode: 'ABC123'
  }, 'id'); // Verificar que 'id' no exista

  console.log('Usuario creado exitosamente');
} catch (error) {
  if (error.name === 'ConditionalCheckFailedException') {
    console.log('El usuario ya existe');
  }
}
update - Actualizar un elemento existente
// Actualizar campos específicos
const updatedUser = await userModel.update(
  { 
    name: 'John Updated',
    status: 'inactive',
    lastUpdated: new Date().toISOString()
  }, // Campos a actualizar
  { id: 'user-123' } // Clave para identificar el elemento
);

console.log(updatedUser); // Elemento con todos los nuevos valores
delete - Eliminar un elemento
// Eliminar por clave simple
const deleteResult = await userModel.delete({
  Key: { id: 'user-123' }
});

// Eliminar por clave compuesta
const deleteProfileResult = await userModel.delete({
  Key: { 
    PK: 'user-123', 
    SK: 'profile' 
  }
});

console.log(deleteResult); // Respuesta de DynamoDB
findAll - Escanear todos los elementos (con filtros opcionales)
// Obtener todos los elementos
const allUsers = await userModel.findAll({});

// Escanear con filtro
const activeUsers = await userModel.findAll({
  filterExpression: '#status = :status',
  expressionAttributeNames: {
    '#status': 'status'
  },
  expressionAttributeValues: {
    ':status': 'active'
  }
});

// Escanear con límite
const limitedUsers = await userModel.findAll({
  limit: 10,
  filterExpression: 'attribute_exists(email)'
});

console.log(activeUsers); // { items: [...] }
queryAll - Consultar por clave de partición
// Consulta básica por partition key
const userSessions = await userModel.queryAll({
  keyConditionExpression: 'PK = :pk',
  expressionAttributeValues: {
    ':pk': 'user-123'
  }
});

// Consulta con rango de sort key
const recentSessions = await userModel.queryAll({
  keyConditionExpression: 'PK = :pk AND SK BETWEEN :start AND :end',
  expressionAttributeValues: {
    ':pk': 'user-123',
    ':start': '2024-01-01',
    ':end': '2024-12-31'
  }
});

// Consulta con ordenamiento descendente y límite
const latestMessages = await userModel.queryAll({
  keyConditionExpression: 'chatId = :chatId',
  expressionAttributeValues: {
    ':chatId': 'chat-123'
  },
  scanIndexForward: false, // Orden descendente
  limit: 50
});

// Consulta en un índice secundario global (GSI)
const usersByEmail = await userModel.queryAll({
  keyConditionExpression: 'email = :email',
  expressionAttributeValues: {
    ':email': 'john@example.com'
  },
  indexKey: 'email-index'
});

console.log(userSessions); // { items: [...] }
queryLast - Obtener el elemento más reciente
// Obtener el mensaje más reciente de un chat
const lastMessage = await userModel.queryLast({
  keyConditionExpression: 'chatId = :chatId',
  expressionAttributeValues: {
    ':chatId': 'chat-123'
  }
});

// Obtener la sesión más reciente de un usuario
const lastSession = await userModel.queryLast({
  keyConditionExpression: 'userId = :userId',
  expressionAttributeValues: {
    ':userId': 'user-123'
  },
  indexKey: 'user-sessions-index'
});

console.log(lastMessage); // Objeto del elemento más reciente o null
queryFirst - Obtener el elemento más antiguo
// Obtener el primer mensaje de un chat
const firstMessage = await userModel.queryFirst({
  keyConditionExpression: 'chatId = :chatId',
  expressionAttributeValues: {
    ':chatId': 'chat-123'
  }
});

// Obtener la primera sesión de un usuario
const firstSession = await userModel.queryFirst({
  keyConditionExpression: 'userId = :userId',
  expressionAttributeValues: {
    ':userId': 'user-123'
  }
});

console.log(firstMessage); // Objeto del elemento más antiguo o null
bulkCreate - Crear múltiples elementos en lote
// Crear múltiples usuarios
const usersToCreate = [
  { id: 'user-200', name: 'Alice', email: 'alice@example.com' },
  { id: 'user-201', name: 'Bob', email: 'bob@example.com' },
  { id: 'user-202', name: 'Charlie', email: 'charlie@example.com' },
  // ... hasta 25 elementos por lote
];

const results = await userModel.bulkCreate({
  rows: usersToCreate,
  chunkSize: 25 // Opcional, por defecto es 25
});

// Procesar resultados
results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`Lote ${index + 1}: ${result.value} elementos procesados`);
  } else {
    console.error(`Lote ${index + 1} falló:`, result.reason);
  }
});

Ejemplos Avanzados

Manejo de errores y reintentos
import { BulkUnprocessedItems } from '@tapila/tapi-lib-dynamo-db/exceptions';

try {
  const results = await userModel.bulkCreate({
    rows: largeDataSet,
    chunkSize: 25
  });
} catch (error) {
  if (error instanceof BulkUnprocessedItems) {
    console.log(`Elementos procesados: ${error.processedItemsCount}`);
    console.log('Elementos no procesados:', error.items);
    // Implementar lógica de reintento
  }
}
Consultas complejas con múltiples filtros
const complexQuery = await userModel.findAll({
  filterExpression: '#status = :status AND #createdAt BETWEEN :startDate AND :endDate AND contains(#tags, :tag)',
  expressionAttributeNames: {
    '#status': 'status',
    '#createdAt': 'createdAt',
    '#tags': 'tags'
  },
  expressionAttributeValues: {
    ':status': 'active',
    ':startDate': '2024-01-01',
    ':endDate': '2024-12-31',
    ':tag': 'premium'
  },
  limit: 100
});
Paginación manual
let lastEvaluatedKey = null;
const allResults = [];

do {
  const response = await userModel.queryAll({
    keyConditionExpression: 'PK = :pk',
    expressionAttributeValues: { ':pk': 'user-data' },
    limit: 50,
    exclusiveStartKey: lastEvaluatedKey
  });
  
  allResults.push(...response.items);
  lastEvaluatedKey = response.lastEvaluatedKey;
} while (lastEvaluatedKey);

console.log(`Total elementos obtenidos: ${allResults.length}`);

Configuración del entorno

Variables de entorno recomendadas
# .env
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
DYNAMODB_ENDPOINT=http://localhost:8000  # Solo para desarrollo local
Docker Compose para DynamoDB Local
# docker-compose.yml
version: '3.8'
services:
  dynamodb-local:
    command: "-jar DynamoDBLocal.jar -sharedDb -optimizeDbBeforeStartup -dbPath ./data"
    image: "amazon/dynamodb-local:latest"
    container_name: dynamodb-local
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal

Testing

Ejemplo de test unitario
import { describe, it, expect, beforeEach } from 'vitest';
import { dynamoConnection, DynamoModel } from '../lib/index.mjs';
import { mockClient } from './mocks/aws.mjs';

describe('DynamoModel', () => {
  let model;

  beforeEach(() => {
    // Para tests unitarios, se puede usar un mock client
    model = new DynamoModel('test-table', mockClient);
  });

  it('should create a user successfully', async () => {
    const userData = {
      id: 'test-user',
      name: 'Test User',
      email: 'test@example.com'
    };

    const result = await model.create(userData);
    expect(result).toBeNull(); // Nuevo elemento creado
  });

  it('should find a user by id', async () => {
    const user = await model.findOne({
      key: { id: 'test-user' }
    });

    expect(user).toEqual({
      id: 'test-user',
      name: 'Test User',
      email: 'test@example.com'
    });
  });
});
Ejemplo de test de integración
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { dynamoConnection, DynamoModel } from '../lib/index.mjs';
import { createTable, deleteTable } from '../testHelpers/manageTables.mjs';

describe('DynamoModel Integration Tests', () => {
  let model;
  const tableName = 'test-table';

  beforeEach(async () => {
    // Crear tabla para testing
    await createTable({ tableName });
    
    // Configurar cliente para DynamoDB Local
    const client = dynamoConnection('us-east-1', 'http://localhost:8000');
    model = new DynamoModel(tableName, client);
  });

  afterEach(async () => {
    // Limpiar tabla después de cada test
    await deleteTable(tableName);
  });

  it('should perform full CRUD operations', async () => {
    const userData = {
      id: 'test-user',
      name: 'Test User',
      email: 'test@example.com'
    };

    // Create
    await model.create(userData);

    // Read
    const user = await model.findOne({ key: { id: 'test-user' } });
    expect(user).toEqual(userData);

    // Update
    const updatedUser = await model.update(
      { name: 'Updated User' },
      { id: 'test-user' }
    );
    expect(updatedUser.name).toBe('Updated User');

    // Delete
    await model.delete({ Key: { id: 'test-user' } });
    const deletedUser = await model.findOne({ key: { id: 'test-user' } });
    expect(deletedUser).toBeNull();
  });
});

API Reference

Constructor
new DynamoModel(tableName, dynamoDBClient)
Métodos disponibles
Método Descripción Retorna
findOne(params) Busca un elemento por clave primaria Promise<Object|null>
create(params, options?) Crea un nuevo elemento Promise<Object|null>
createIfNotExists(params, key, options?) Crea solo si no existe Promise<Object|null>
update(setParams, whereParams) Actualiza un elemento existente Promise<Object|null>
delete(params) Elimina un elemento Promise<Object>
findAll(params) Escanea la tabla con filtros opcionales Promise<{items: Array}>
queryAll(params) Consulta por clave de partición Promise<{items: Array}>
queryLast(params) Obtiene el elemento más reciente Promise<Object|null>
queryFirst(params) Obtiene el elemento más antiguo Promise<Object|null>
bulkCreate(params) Crea múltiples elementos en lote Promise<Array>

Contribución

  1. Fork el repositorio
  2. Crea una rama para tu feature (git checkout -b feature/amazing-feature)
  3. Commit tus cambios (git commit -m 'Add some amazing feature')
  4. Push a la rama (git push origin feature/amazing-feature)
  5. Abre un Pull Request

Licencia

Este proyecto está bajo la licencia MIT. Ver el archivo LICENSE para más detalles.

Soporte

Si encuentras algún problema o tienes preguntas:

Keywords