nnestjs-drizzle-crud
Guides

Bulk operations

massCreate, massUpdate, massSoftDelete, massRestore, massDelete — all transactional with rollback on failure.

Bulk operations run inside a single database transaction. If any row fails, the entire operation is rolled back and a BulkOperationException is thrown, carrying the per-row errors.

The five methods

await service.massCreate([dto1, dto2, dto3]);
await service.massUpdate([1, 2, 3], { status: 'archived' });
await service.massSoftDelete([1, 2, 3]);
await service.massRestore([1, 2, 3]);
await service.massDelete([1, 2, 3]);
MethodReturnsFailure mode
massCreate(data[], options?)Promise<T[]>Throws BulkOperationException on the first failing row.
massUpdate(ids[], data, options?)Promise<T[]>Same.
massSoftDelete(ids[], options?)Promise<boolean>Same.
massRestore(ids[], options?)Promise<T[]>Same.
massDelete(ids[], options?)Promise<boolean>Same.

Transactional semantics

All bulk methods wrap their work in a single transaction. If any row fails (constraint violation, FK error, validation, etc.), the transaction is rolled back — no partial writes are visible to subsequent reads.

A bulk operation that succeeds returns its full result. A bulk operation that fails throws BulkOperationException and none of the rows are modified. Use this as a guarantee, not just an optimization.

BulkOperationException

import { BulkOperationException } from 'nestjs-drizzle-crud';

try {
  await users.massCreate([{ name: 'a' }, { name: 'b' /* missing required email */ }]);
} catch (err) {
  if (err instanceof BulkOperationException) {
    // err.errors: per-row errors
    // err.index: index of the failing row
    console.error(`Row ${err.index} failed:`, err.errors);
  }
  throw err;
}

Catch BulkOperationException to surface per-row errors to the client. Otherwise let it propagate to your exception filter for a generic 500.

Lifecycle hooks during bulk operations

The standard beforeCreate / afterCreate (and the corresponding update / delete / softDelete / restore hooks) fire once per row, in array order. A massCreate([a, b, c]) calls beforeCreate three times before the insert, then the insert, then afterCreate three times.

If any row's beforeCreate throws, the operation is aborted before the insert — no rows are written.

Passing options

All bulk methods accept the same SqlOperationOptions as their non-bulk counterparts. In particular, you can pass a custom transaction:

await service.executeSqlTransaction(async (tx) => {
  await service.massCreate(dtos, { transaction: tx });
  await otherService.massUpdate(otherIds, { flag: true }, { transaction: tx });
});

See Transactions for the full pattern.

Limits and pagination

Bulk methods don't paginate. They accept the full array in memory and execute it as a single transaction. For very large bulk operations (thousands of rows), consider:

  • Chunking on the caller side — process arrays in batches of 100–500 rows and call massCreate per batch.
  • Switching to a streaming insert — write a custom method that uses Drizzle's db.insert(...).values(chunk).execute() chain with explicit chunking.

The cap is on the database side, not the package — single transactions holding thousands of row locks can starve other connections.

Next

On this page