NestboltNestbolt

@nestbolt/sortable

Introduction

Automatic position management for NestJS TypeORM entities -- auto-assign positions on insert, run safe move and reorder operations, and keep ordering gap-free.

@nestbolt/sortable adds automatic position management to NestJS TypeORM entities. Decorate any entity with @Sortable() and an integer position column, and new rows are auto-assigned the next position on insert. Move operations (moveUp, moveDown, moveTo, moveToTop, moveToBottom) and bulk reorder() keep ordering gap-free and run inside a transaction so concurrent writers can't tear the sequence.

Key Features

  • Auto-positioning on insert -- a TypeORM subscriber assigns position = MAX + 1 when the column is left unset, using the same connection as the insert so the read+write pair is atomic.
  • Safe move operations -- moveTo, moveUp, moveDown, moveToTop, moveToBottom clamp the target to [startPosition, maxPosition] and shift the affected window in a transaction so there are never gaps or duplicate positions.
  • UNIQUE-constraint friendly -- the move and reorder algorithms park the moving row at a scratch position before settling, so a UNIQUE(position) or UNIQUE(group, position) constraint won't collide mid-shift.
  • Bulk reorder -- reorder(Entity, orderedIds) settles many rows at once with full id-existence and group-membership validation; the call is rejected up front if the inputs are inconsistent.
  • Group support -- @Sortable({ groupBy: "listId" }) keeps a separate sequence per group value, scoped automatically from the entity's group field.
  • Entity mixin -- SortableMixin adds moveUp(), moveDown(), moveTo(), moveToTop(), moveToBottom(), getPosition(), and getPositionField() directly on entity instances.
  • Event system -- emits sortable.position-changed and sortable.reordered via @nestjs/event-emitter (optional).
  • Identifier safety -- field and groupBy values are validated against /^[A-Za-z_][A-Za-z0-9_]*$/ before any raw SQL fragment is built, so configuration can't introduce SQL injection.

Architecture

The package is built around several core components:

SortableModule registers all providers globally. You call SortableModule.forRoot() or SortableModule.forRootAsync() once in your root module, and SortableService becomes available everywhere via dependency injection.

SortableService is the primary API surface. It provides moveTo(), moveUp(), moveDown(), moveToTop(), moveToBottom(), reorder(), getMaxPosition(), getNextPosition(), and isSortable(). Move operations run inside dataSource.transaction(...) (or a caller-supplied EntityManager), and identifier-based SQL fragments are built from validated property names mapped to their database column names via TypeORM metadata.

@Sortable() decorator marks an entity class as sortable and stores the position field name and optional group-by column on the entity's metadata.

SortableMixin is an optional mixin that adds convenience methods directly to your entity instances so you can call task.moveUp() instead of service.moveUp(Task, task.id).

SortableSubscriber registers itself on the TypeORM DataSource and assigns the next position before insert when the entity is decorated and the position column is left unset.

Basic Usage

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Sortable, SortableMixin } from "@nestbolt/sortable";

class TaskBase {
  id!: string;
}

@Sortable()
@Entity("tasks")
export class Task extends SortableMixin(TaskBase) {
  @PrimaryGeneratedColumn("uuid")
  declare id: string;

  @Column()
  name!: string;

  @Column({ type: "int", default: 0 })
  position!: number;
}

// New rows are auto-positioned: 0, 1, 2, ...
await repo.save(repo.create({ name: "First" }));
await repo.save(repo.create({ name: "Second" }));
await repo.save(repo.create({ name: "Third" }));

// Move with the mixin
await task.moveUp();
await task.moveTo(0);
await task.moveToBottom();

// Or with the service
await sortable.reorder(Task, [thirdId, firstId, secondId]);

Next Steps

  • Installation -- install the package and its peer dependencies.
  • Quick Start -- set up the module, decorate an entity, and reorder your first record in minutes.
  • Configuration -- forRoot, forRootAsync, and the full SortableService API.
  • Decorator -- @Sortable() options reference, including field and groupBy.
  • Mixin -- add moveUp(), moveDown(), moveTo(), and friends directly to your entities.
  • Subscriber -- how the auto-position-on-insert subscriber works and when it skips.
  • Events -- listen to sortable.position-changed and sortable.reordered.