Skip to content

Repository files navigation

@nestbolt/sortable

Automatic position management and ordering for NestJS entities with TypeORM.

npm versionnpm downloadstestslicense


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()exportclassTaskextendsSortableMixin(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

Install the package via npm:

npm install @nestbolt/sortable

Or via yarn:

yarn add @nestbolt/sortable

Or via pnpm:

pnpm add @nestbolt/sortable

Peer 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.0

Optional

npm install @nestjs/event-emitter # For sortable.position-changed events

Quick Start

1. Register the module in your AppModule

import{SortableModule}from"@nestbolt/sortable";
@Module({imports: [TypeOrmModule.forRoot({/* ... */}),SortableModule.forRoot(),],})exportclassAppModule{}

2. Mark entities as sortable

import{Sortable,SortableMixin}from"@nestbolt/sortable";
@Entity("tasks")
@Sortable()exportclassTaskextendsSortableMixin(BaseEntity){
@PrimaryGeneratedColumn("uuid")id!: string;
@Column()name!: string;
@Column({type: "int",default: 0})position!: number;}

3. Entities get auto-positioned on insert

consttask1=awaitrepo.save(repo.create({name: "First"}));// position: 0consttask2=awaitrepo.save(repo.create({name: "Second"}));// position: 1consttask3=awaitrepo.save(repo.create({name: "Third"}));// position: 2// ReorderawaitsortableService.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
OptionTypeDefaultDescription
fieldstring"position"Column name that stores the sort position
groupBystringColumn 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-assignedconsttask=awaitrepo.save(repo.create({name: "New Task"}));// task.position === next available position// Explicitly set — subscriber skipsconsttask=awaitrepo.save(repo.create({name: "Custom",position: 99}));// task.position === 99

Move Operations

import{SortableService}from"@nestbolt/sortable";// Move to specific positionawaitsortableService.moveTo(Task,taskId,0);// Move up/down by one positionawaitsortableService.moveUp(Task,taskId);awaitsortableService.moveDown(Task,taskId);// Move to extremesawaitsortableService.moveToTop(Task,taskId);awaitsortableService.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:

awaitsortableService.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")exportclassTask{
@Column()listId!: string;
@Column({type: "int",default: 0})position!: number;}

Items in different groups are independently ordered:

awaitrepo.save(repo.create({name: "A",listId: "list-1"}));// position: 0awaitrepo.save(repo.create({name: "B",listId: "list-1"}));// position: 1awaitrepo.save(repo.create({name: "C",listId: "list-2"}));// position: 0// Move within groupawaitsortableService.moveUp(Task,taskId,"list-1");

Entity Mixin

The SortableMixin adds convenience methods directly on your entity:

@Entity("tasks")
@Sortable()exportclassTaskextendsSortableMixin(BaseEntity){// ...}// Usageconsttask=awaittaskRepo.findOneBy({ id });awaittask.moveUp();awaittask.moveDown();awaittask.moveTo(0);awaittask.moveToTop();awaittask.moveToBottom();constpos=task.getPosition();
MethodReturnsDescription
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()numberGet current position
getPositionField()stringGet position column name

Events

When @nestjs/event-emitter is installed, the package emits:

EventPayloadWhen
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()exportclassTaskService{constructor(privatereadonlysortableService: SortableService){}asyncreorderTasks(orderedIds: string[]){awaitthis.sortableService.reorder(Task,orderedIds);}}
MethodReturnsDescription
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)booleanCheck for @Sortable metadata

Configuration Options

OptionTypeDefaultDescription
fieldstring"position"Default position column name
startPositionnumber0Starting position for new items

Testing

npm test

Run tests in watch mode:

npm run test:watch

Generate coverage report:

npm run test:cov

Changelog

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.

License

The MIT License (MIT). Please see License File for more information.

About

Automatic position management and ordering for NestJS entities with TypeORM.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages