Skip to content

Conversation

omvmike
Copy link
Owner

@omvmike omvmike commented Jun 22, 2025

🔑 Add Composite Primary Key Support

Fixes #1

Summary

This PR adds comprehensive composite primary key support to the fake-entity-service library for both TypeORM and Sequelize. This major enhancement enables the library to work seamlessly with complex database schemas that use multi-column primary keys, while maintaining full backward compatibility with single primary keys.

This implementation directly addresses Issue #1 which requested "Automatic detection of Model primary keys for TypeOrm" and investigation of multi-column keys. Our solution goes beyond the original scope by providing automatic detection for both TypeORM AND Sequelize.

Key Features Added

  • Automatic Primary Key Detection for both TypeORM and Sequelize
  • Full Composite Primary Key Support with CRUD operations
  • Enhanced Error Handling with detailed validation messages
  • Comprehensive Test Coverage with 900+ lines of integration tests
  • Backward Compatibility maintained for existing single primary key usage

🎯 Motivation

Many database schemas use composite primary keys, especially in:

  • Junction/pivot tables (many-to-many relationships)
  • Temporal data models
  • Legacy database systems
  • Domain-specific requirements

Before this PR, the library only supported single primary keys, limiting its usefulness in complex scenarios.

📋 Changes Overview

Core Enhancements

TypeORM Service (src/typeorm-fake-entity.service.ts)

  • Replaced idFieldName: string with idFieldNames: string[]
  • Added automatic primary key detection from entity metadata
  • Implemented composite key CRUD operations
  • Enhanced error handling with entity context

Sequelize Service (src/sequelize-fake-entity.service.ts)

  • Enhanced composite key cleanup logic
  • Improved error messages with entity names
  • Added composite key finder methods

New Methods Added

// Check if entity uses composite primary key
hasCompositeId(): boolean

// Get all primary key field names
getIdFieldNames(): string[]

// Find entity by composite primary key values
findByCompositeKey(keyValues: Record<string, any>): Promise<TEntity>

// Get TypeORM primary column metadata
getPrimaryColumns() // TypeORM only

Test Coverage

  • Primary Key Detection Tests: Validates automatic detection for both ORMs
  • Composite Key CRUD Tests: Full create, read, update, delete operations
  • Integration Tests: Real database operations with PostgreSQL
  • Error Handling Tests: Edge cases and validation scenarios
  • Cleanup Tests: Entity tracking and deletion with composite keys

🔧 Breaking Changes

TypeORM Users

// Before (manual configuration)
export class FakeUserService extends TypeormFakeEntityService<User> {
    public idFieldName = 'uuid'; // Single field only
}

// After (automatic detection + optional override)
export class FakeUserService extends TypeormFakeEntityService<User> {
    // Automatic detection works out of the box
    // Override only if needed:
    public idFieldNames = ['customId']; // Array for single or composite keys
}

Migration Guide

  1. Automatic Detection: Most users need no changes - primary keys are detected automatically
  2. Manual Override: Replace idFieldName with idFieldNames array if you were manually setting it
  3. Composite Keys: No additional configuration needed - works automatically

📊 Test Results

# Integration Tests
✓ TypeORM Primary Key Detection (8 tests)
✓ Sequelize Primary Key Detection (8 tests)  
✓ TypeORM Composite Key Operations (15 tests)
✓ Sequelize Composite Key Operations (15 tests)
✓ TypeORM Integration Tests (12 tests)
✓ Sequelize Integration Tests (12 tests)

Total: 70+ new tests, 2,290+ lines added

🔍 Code Examples

Single Primary Key (Backward Compatible)

// Works exactly as before
const users = await fakeUserService.createMany(5);
await fakeUserService.cleanup(); // Deletes all created users

Composite Primary Key (New)

// Entity with composite primary key
@Entity()
class Follower {
  @PrimaryColumn()
  userId: number;
  
  @PrimaryColumn() 
  followerId: number;
}

// Usage (automatic detection)
const followers = await fakeFollowerService.createMany(3, {
  userId: 1,
  followerId: 2  
});

// Find by composite key
const follower = await fakeFollowerService.findByCompositeKey({
  userId: 1,
  followerId: 2
});

// Cleanup handles composite keys
await fakeFollowerService.cleanup();

🛡️ Error Handling

Enhanced error messages provide better debugging:

// Before
Error: Id field "id" is empty

// After  
Error: Primary key field "userId" is empty or null in entity Follower
Error: No primary keys detected for entity User. Please ensure the entity has @PrimaryColumn or @PrimaryGeneratedColumn decorators.

📦 File Changes

  • src/typeorm-fake-entity.service.ts - Major rewrite with composite key support
  • src/sequelize-fake-entity.service.ts - Enhanced cleanup and error handling
  • tests/ - 900+ lines of comprehensive test coverage
  • package.json - Version bump to 0.10.0
  • README.md - Updated documentation with composite key examples
  • CHANGELOG.md - Detailed changelog entry

🎯 Test Plan

  • All existing tests pass (backward compatibility)
  • New composite key tests pass
  • Integration tests with real PostgreSQL database
  • Error handling validation
  • Transaction support verification
  • Entity cleanup verification

🚀 Impact

This enhancement significantly expands the library's capabilities:

  • Broader Database Support: Works with complex schema designs
  • Enterprise Ready: Handles real-world database patterns
  • Developer Experience: Automatic detection reduces configuration
  • Maintainability: Better error messages improve debugging

Version: 0.10.0
Type: Minor release (new features, backward compatible except breaking changes noted)
Test Coverage: Comprehensive integration and unit tests
Documentation: Updated README and CHANGELOG

@omvmike omvmike requested a review from Copilot June 22, 2025 13:44
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full composite primary key support to both TypeORM and Sequelize services, including automatic detection, CRUD operations, error handling, and documentation updates.

  • Automatic detection of primary keys from ORM metadata
  • Composite key CRUD support and detailed validation messages
  • Updated tests, README, and changelog for multi-column keys

Reviewed Changes

Copilot reviewed 12 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/typeorm-fake-entity.service.ts Added automatic key detection, composite CRUD logic
src/sequelize-fake-entity.service.ts Extended cleanup/delete, but missing composite getId
tests/typeorm-primary-key-detection.spec.ts New tests for TypeORM composite and single-key paths
tests/sequelize-primary-key-detection.spec.ts New tests for Sequelize composite and single-key paths
README.md Documented composite key support and detection

if (this.hasCompositeId()) {
// For composite keys, need deep comparison of objects
this.entityIds = this.entityIds.filter(entityId => {
return !entityIds.some(deletedId => {
Copy link
Preview

Copilot AI Jun 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delete method references entityIds instead of the method parameter ids, which will cause a ReferenceError. It should use ids.some(...) to filter out deleted IDs.

Copilot uses AI. Check for mistakes.

if (this.hasCompositeId()) {
return this.pickKeysFromObject(e);
}
const idFieldName = this.getIdFieldNames()[0];
Copy link
Preview

Copilot AI Jun 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Composite primary keys aren’t handled in getId; it only returns a single key. You need to detect hasCompositeId() and return an object of all key fields (e.g., via a shared pickKeysFromObject).

Copilot uses AI. Check for mistakes.

Comment on lines +365 to +369
for (const where of whereConditions) {
const res = await repo.delete(where);
totalAffected += res.affected || 0;
}
return totalAffected;
Copy link
Preview

Copilot AI Jun 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Deleting composite-key entities one by one may cause N+1 queries. Consider batching deletes with a single query or using a QueryBuilder to improve performance.

Suggested change
for (const where of whereConditions) {
const res = await repo.delete(where);
totalAffected += res.affected || 0;
}
return totalAffected;
const queryBuilder = repo.createQueryBuilder();
queryBuilder.delete().from(this.repository.target);
whereConditions.forEach((where, index) => {
if (index === 0) {
queryBuilder.where(where);
} else {
queryBuilder.orWhere(where);
}
});
const res = await queryBuilder.execute();
return res.affected || 0;

Copilot uses AI. Check for mistakes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Automatic detection of Model primary keys for TypeOrm
1 participant