JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 99
  • Score
    100M100P100Q82501F
  • License ISC

A library to manage DYnamoDB

Package Exports

  • @npmtapi/tapi-lib-dynamo-db
  • @npmtapi/tapi-lib-dynamo-db/lib/index.mjs

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@npmtapi/tapi-lib-dynamo-db) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

TAPI DynamoDB Library 2.8.0

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: