@nestbolt/sortable
v0.1.0
Published
Automatic position management and ordering for NestJS entities with TypeORM.
Maintainers
Readme
This package provides automatic position management for NestJS that auto-assigns positions to new entities and provides move operations (up, down, to position, top, bottom) with gap-free reordering.
Once installed, using it is as simple as:
@Entity("tasks")
@Sortable()
export class Task extends SortableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() name!: string;
@Column({ type: "int", default: 0 }) position!: number;
}
// New entities get auto-positioned: 0, 1, 2, ...
// Move with: await sortableService.moveUp(Task, taskId);Table of Contents
- Installation
- Quick Start
- Module Configuration
- Using the @Sortable() Decorator
- Auto Position on Insert
- Move Operations
- Bulk Reorder
- Group Support
- Entity Mixin
- Events
- Using the Service Directly
- Configuration Options
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Installation
Install the package via npm:
npm install @nestbolt/sortableOr via yarn:
yarn add @nestbolt/sortableOr via pnpm:
pnpm add @nestbolt/sortablePeer Dependencies
This package requires the following peer dependencies, which you likely already have in a NestJS project:
@nestjs/common ^10.0.0 || ^11.0.0
@nestjs/core ^10.0.0 || ^11.0.0
typeorm ^0.3.0
reflect-metadata ^0.1.13 || ^0.2.0Optional
npm install @nestjs/event-emitter # For sortable.position-changed eventsQuick Start
1. Register the module in your AppModule
import { SortableModule } from "@nestbolt/sortable";
@Module({
imports: [
TypeOrmModule.forRoot({ /* ... */ }),
SortableModule.forRoot(),
],
})
export class AppModule {}2. Mark entities as sortable
import { Sortable, SortableMixin } from "@nestbolt/sortable";
@Entity("tasks")
@Sortable()
export class Task extends SortableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() name!: string;
@Column({ type: "int", default: 0 }) position!: number;
}3. Entities get auto-positioned on insert
const task1 = await repo.save(repo.create({ name: "First" })); // position: 0
const task2 = await repo.save(repo.create({ name: "Second" })); // position: 1
const task3 = await repo.save(repo.create({ name: "Third" })); // position: 2
// Reorder
await sortableService.moveToTop(Task, task3.id);
// Result: Third (0), First (1), Second (2)Module Configuration
The module is registered globally — you only need to import it once.
Static Configuration (forRoot)
SortableModule.forRoot({
field: "position",
startPosition: 0,
});Async Configuration (forRootAsync)
SortableModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
field: config.get("sortable.field", "position"),
startPosition: config.get("sortable.startPosition", 0),
}),
});Using the @Sortable() Decorator
The @Sortable() class decorator marks an entity for automatic position management:
@Sortable() // defaults: field="position", no grouping
@Sortable({ field: "sortOrder" }) // custom position field name
@Sortable({ groupBy: "categoryId" }) // separate ordering per group
@Sortable({ field: "position", groupBy: "listId" }) // both options| Option | Type | Default | Description |
| --------- | -------- | ------------ | ---------------------------------------------- |
| field | string | "position" | Column name that stores the sort position |
| groupBy | string | — | Column to group by for independent sort orders |
Auto Position on Insert
New entities automatically get the next position assigned via the TypeORM subscriber. If a position is explicitly set, the auto-assignment is skipped:
// Auto-assigned
const task = await repo.save(repo.create({ name: "New Task" }));
// task.position === next available position
// Explicitly set — subscriber skips
const task = await repo.save(repo.create({ name: "Custom", position: 99 }));
// task.position === 99Move Operations
import { SortableService } from "@nestbolt/sortable";
// Move to specific position
await sortableService.moveTo(Task, taskId, 0);
// Move up/down by one position
await sortableService.moveUp(Task, taskId);
await sortableService.moveDown(Task, taskId);
// Move to extremes
await sortableService.moveToTop(Task, taskId);
await sortableService.moveToBottom(Task, taskId);All move operations automatically shift surrounding items to maintain gap-free ordering.
Bulk Reorder
Set positions for multiple entities at once by passing an ordered array of IDs:
await sortableService.reorder(Task, [thirdId, firstId, secondId]);
// Result: Third (0), First (1), Second (2)Group Support
Maintain separate sort orders per group using the groupBy option:
@Sortable({ groupBy: "listId" })
@Entity("tasks")
export class Task {
@Column() listId!: string;
@Column({ type: "int", default: 0 }) position!: number;
}Items in different groups are independently ordered:
await repo.save(repo.create({ name: "A", listId: "list-1" })); // position: 0
await repo.save(repo.create({ name: "B", listId: "list-1" })); // position: 1
await repo.save(repo.create({ name: "C", listId: "list-2" })); // position: 0
// Move within group
await sortableService.moveUp(Task, taskId, "list-1");Entity Mixin
The SortableMixin adds convenience methods directly on your entity:
@Entity("tasks")
@Sortable()
export class Task extends SortableMixin(BaseEntity) {
// ...
}
// Usage
const task = await taskRepo.findOneBy({ id });
await task.moveUp();
await task.moveDown();
await task.moveTo(0);
await task.moveToTop();
await task.moveToBottom();
const pos = task.getPosition();| Method | Returns | Description |
| ------------------ | --------------- | ------------------------ |
| moveUp() | Promise<void> | Move up by one position |
| moveDown() | Promise<void> | Move down by one |
| moveTo(position) | Promise<void> | Move to exact position |
| moveToTop() | Promise<void> | Move to first position |
| moveToBottom() | Promise<void> | Move to last position |
| getPosition() | number | Get current position |
| getPositionField() | string | Get position column name |
Events
When @nestjs/event-emitter is installed, the package emits:
| Event | Payload | When |
| --------------------------- | -------------------------------------------------- | --------------------------- |
| sortable.position-changed | { entityType, entityId, oldPosition, newPosition } | After a position change |
| sortable.reordered | { entityType, items: [{ id, position }] } | After a bulk reorder |
import { SORTABLE_EVENTS, PositionChangedEvent } from "@nestbolt/sortable";
import { OnEvent } from "@nestjs/event-emitter";
@OnEvent(SORTABLE_EVENTS.POSITION_CHANGED)
handlePositionChanged(event: PositionChangedEvent) {
console.log(`${event.entityType}#${event.entityId} moved from ${event.oldPosition} to ${event.newPosition}`);
}Using the Service Directly
Inject SortableService for position management:
import { SortableService } from "@nestbolt/sortable";
@Injectable()
export class TaskService {
constructor(private readonly sortableService: SortableService) {}
async reorderTasks(orderedIds: string[]) {
await this.sortableService.reorder(Task, orderedIds);
}
}| Method | Returns | Description |
| ----------------------------------------- | ---------------- | --------------------------- |
| moveTo(Entity, id, position, group?) | Promise<void> | Move to exact position |
| moveUp(Entity, id, group?) | Promise<void> | Move up by one |
| moveDown(Entity, id, group?) | Promise<void> | Move down by one |
| moveToTop(Entity, id, group?) | Promise<void> | Move to first position |
| moveToBottom(Entity, id, group?) | Promise<void> | Move to last position |
| reorder(Entity, orderedIds) | Promise<void> | Bulk reorder by ID array |
| getMaxPosition(Entity, group?) | Promise<number> | Get highest position |
| getNextPosition(Entity, group?) | Promise<number> | Get next available position |
| isSortable(Entity) | boolean | Check for @Sortable metadata |
Configuration Options
| Option | Type | Default | Description |
| --------------- | -------- | ------------ | ------------------------------------ |
| field | string | "position" | Default position column name |
| startPosition | number | 0 | Starting position for new items |
Testing
npm testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covChangelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.
Credits
- Inspired by Laravel's Eloquent ordered model packages and spatie/eloquent-sortable
License
The MIT License (MIT). Please see License File for more information.
