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