Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

All notable changes to this project will be documented in this file.

## [0.10.0] - 2025-06-22
### Added
- **Composite Primary Key Support**: Full support for multi-column primary keys in both TypeORM and Sequelize
- **Automatic Primary Key Detection**: Both ORMs now automatically detect primary keys from entity metadata
- TypeORM: Reads from `@PrimaryColumn` and `@PrimaryGeneratedColumn` decorators
- Sequelize: Uses model's `primaryKeyAttributes` property
- **Enhanced TypeORM Service**: Complete rewrite of primary key handling with composite key support
- **New Methods**: Added `findByCompositeKey()`, `hasCompositeId()`, `getIdFieldNames()`, and `getPrimaryColumns()` methods
- **Improved Error Handling**: Enhanced validation and error messages for primary key operations
- **Comprehensive Test Coverage**: Added 900+ lines of integration tests covering:
- Single and composite primary key detection
- CRUD operations with composite keys
- Error handling and edge cases
- Entity cleanup with composite keys

### Changed
- **BREAKING**: TypeORM service now uses `idFieldNames: string[]` instead of `idFieldName: string`
- **BREAKING**: Primary key detection is now automatic; manual override requires `idFieldNames` array
- **Enhanced**: Improved entity cleanup logic for both single and composite primary keys
- **Enhanced**: Better error messages with entity name context

### Fixed
- Fixed entity cleanup to properly handle composite primary keys
- Fixed primary key validation to handle null/undefined values
- Improved transaction handling for composite key operations

## [0.9.2] - 2025-05-12
### Added
- Introduced the `clone()` method to core and all services, enabling creation of isolated service instances with shared repository and empty state.
Expand Down
81 changes: 60 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ But since it's a TypeScript library, it has quite different syntax.
## Key Features

- πŸ”„ **ORM Agnostic** - Works with both Sequelize and TypeORM
- πŸ”‘ **Composite Primary Key Support** - Full support for single and multi-column primary keys
- πŸ” **Automatic Primary Key Detection** - Automatically detects primary keys from entity metadata
- 🧩 **Relationship Support** - Easily define parent-child and nested relationships
- πŸ§ͺ **Testing-Focused** - Designed specifically for integration and e2e tests
- 🧹 **Automatic Cleanup** - Tracks and cleans up created entities
Expand Down Expand Up @@ -607,29 +609,60 @@ this code will delete all users created by the `fakeUserService` service.

#### Primary keys

As you can see in the examples above, we need to describe primary key column name for the entity model
to track created entities and to delete them later.
The library automatically detects primary keys from your entity metadata for both TypeORM and Sequelize.

Primary key description is ORM specific.
**Automatic Primary Key Detection:**
- **TypeORM**: Reads `@PrimaryColumn` and `@PrimaryGeneratedColumn` decorators from entity metadata
- **Sequelize**: Uses the model's `primaryKeyAttributes` property
- **Composite Keys**: Full support for multi-column primary keys in both ORMs

For Sequelize we support automatic detection of primary keys both for single column and multi-column primary keys.
see [Sequelize specific features](#sequelize-specific-features) section below.
The library tracks created entities using their primary key values and provides cleanup functionality:

Unfortunately, automatic detection of primary keys is not applied for TypeORM version of the library.
Thus, we use `id` field as a default primary key column for TypeORM.
But you can override it by passing `idFieldName` property to your service class:
```typescript
// For single primary key entities
const users = await fakeUserService.createMany(5);
await fakeUserService.cleanup(); // Deletes all created users

// For composite primary key entities
const followers = await fakeFollowerService.createMany(3, {
userId: 1,
followerId: 2
});
await fakeFollowerService.cleanup(); // Handles composite key cleanup
```

**Working with Composite Primary Keys:**

```typescript
import {TypeormFakeEntityService} from "./typeorm-fake-entity.service";
// Find entity by composite key
const follower = await fakeFollowerService.findByCompositeKey({
userId: 1,
followerId: 2
});

// Check if entity has composite primary key
if (fakeFollowerService.hasCompositeId()) {
console.log('Entity uses composite primary key');
console.log('Key fields:', fakeFollowerService.getIdFieldNames());
}
```

**Manual Override (if needed):**
While automatic detection works for most cases, you can override the primary key fields:

```typescript
// TypeORM
export class FakeUserService extends TypeormFakeEntityService<User> {
public idFieldName = 'uuid';
public idFieldNames = ['uuid']; // Override detected primary keys
// ...
}

// Sequelize
export class FakeUserService extends SequelizeFakeEntityService<User> {
public idFieldNames = ['customId']; // Override detected primary keys
// ...
// constructor and other methods
}
```
Multi-column primary keys are not supported for `TypeormFakeEntityService` yet.
```

### Callbacks

Expand Down Expand Up @@ -695,20 +728,26 @@ await cloneB.create(); // Creates a user 'Bob' in cloneB
> **Note:** The `clone()` method assumes your service constructor accepts a repository as the first argument and that a `repository` property exists. If your subclass uses a different signature, override `clone()` accordingly.


## Sequelize specific features
- Use Sequelize's primary keys detection. The library uses Sequelize's model `primaryKeyAttributes` property to detect primary keys.
> If you need to override it, you can pass `idFieldName` property to your service class:
## ORM-Specific Features

- The library can work with multi-column primary keys. It also Sequelize's model `primaryKeyAttributes` property to detect them.
### Sequelize Features
- **Automatic Primary Key Detection**: Uses Sequelize's model `primaryKeyAttributes` property to detect both single and composite primary keys
- **Composite Primary Key Support**: Full CRUD operations support for multi-column primary keys
- **Sequelize Relations**: Integration with Sequelize's built-in associations for nested entity creation

- The library can work with Sequelize's relations. If you described relations in your model, the library will use them to create nested entities.
> For example, if you have `User` and `Notification` models and `User.hasMany(Notification)` relation, you can describe `withNotifications` method from previous example like below:
### TypeORM Features
- **Automatic Primary Key Detection**: Reads primary key metadata from `@PrimaryColumn` and `@PrimaryGeneratedColumn` decorators
- **Composite Primary Key Support**: Complete support for multi-column primary keys with automatic detection
- **Enhanced Error Handling**: Detailed validation messages for primary key operations

### Using Sequelize Relations
If you have described relations in your Sequelize model, the library can use them to create nested entities:
> For example, if you have `User` and `Notification` models and `User.hasMany(Notification)` relation, you can describe `withNotifications` method like below:
```typescript
export class FakePostService extends SequelizeFakeEntityService<Post> {
export class FakeUserService extends SequelizeFakeEntityService<User> {
// constructor and other methods
// ...


withNotifications(fakeNotificationService: FakeNotificationService, count: number, customFields?: Partial<Notification>): FakeUserService {
this.nestedEntities.push({
service: fakeNotificationService,
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fake-entity-service",
"version": "0.9.3",
"version": "0.10.0",
"description": "A fake database entities service for testing",
"author": {
"email": "[email protected]",
Expand Down Expand Up @@ -61,8 +61,8 @@
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"rootDir": ".",
"testRegex": "tests/.*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
Expand Down
2 changes: 2 additions & 0 deletions sequelize/migrations/0000000-initial.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ const migrationCommands = [
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
allowNull: false,
primaryKey: true,
type: Sequelize.INTEGER,
},
follower_id: {
Expand All @@ -177,6 +178,7 @@ const migrationCommands = [
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
allowNull: false,
primaryKey: true,
type: Sequelize.INTEGER,
},
created_at: {
Expand Down
54 changes: 48 additions & 6 deletions src/sequelize-fake-entity.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ export class SequelizeFakeEntityService<TEntity extends Model> extends FakeEntit
}
const idFieldName = this.getIdFieldNames()[0];
Copy link

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.

const idValue = e[idFieldName];
if (idValue === undefined) {
throw new Error(`Id field "${idFieldName}" is empty`)
if (idValue === undefined || idValue === null) {
throw new Error(`Primary key field "${idFieldName}" is empty or null in entity ${this.repository.name}`);
}
return e[this.getIdFieldNames()[0]];
}
Expand Down Expand Up @@ -337,11 +337,25 @@ export class SequelizeFakeEntityService<TEntity extends Model> extends FakeEntit
where,
transaction: tx,
});
}, transaction).then((deletionResult) => {
}, transaction).then((affectedCount) => {
// Remove deleted entity IDs from the entityIds array
this.entityIds = [];
return deletionResult;
})
if (this.hasCompositeId()) {
// For composite keys, need deep comparison of objects
this.entityIds = this.entityIds.filter(entityId => {
return !entityIds.some(deletedId => {
Copy link

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.

// Check if all key fields match
return this.getIdFieldNames().every(field =>
entityId[field] === deletedId[field]
);
});
});
} else {
// For single keys, use simple includes
this.entityIds = this.entityIds.filter(id => !entityIds.includes(id));
}
return affectedCount;
});

}

/**
Expand Down Expand Up @@ -381,4 +395,32 @@ export class SequelizeFakeEntityService<TEntity extends Model> extends FakeEntit
});
return this;
}

/**
* Build where conditions for composite primary keys
*/
private buildCompositeKeyWhere(keyValues: Record<string, any>): any {
const where = {};
for (const [key, value] of Object.entries(keyValues)) {
if (!this.getIdFieldNames().includes(key)) {
throw new Error(`Invalid primary key field "${key}" for entity ${this.repository.name}`);
}
where[key] = value;
}
return where;
}

/**
* Find entity by composite primary key
*/
public async findByCompositeKey(keyValues: Record<string, any>, transaction?: Transaction): Promise<TEntity | undefined> {
return this.withTransaction(async (tx) => {
const where = this.buildCompositeKeyWhere(keyValues);
const result = await this.repository.findOne({
where,
transaction: tx
});
return result || undefined; // Convert null to undefined
}, transaction);
}
}
Loading