Skip to main content

Overview

AggregateChanges is the object returned by aggregate.getChanges(). It contains all detected operations (creates, updates, deletes) and provides methods to query, filter, and batch them for persistence.
const changes = order.getChanges();

// AggregateChanges provides:
// - Query methods (hasCreates, hasUpdates, hasDeletes)
// - Access methods (creates, updates, deletes)
// - Filtering (for)
// - Batching (toBatchOperations)
// - Iteration (operations, toArray)

Operation Types

CreateOperation

Represents a new entity that needs to be inserted:
interface CreateOperation<T> {
  type: "create";
  entity: string;      // Entity name (e.g., "OrderItem")
  data: T;             // The entity instance
  depth: number;       // Depth in aggregate tree
  parentId?: string;   // Parent entity ID (for FK)
  parentEntity?: string; // Parent entity name
}

UpdateOperation

Represents an existing entity with modified properties:
interface UpdateOperation<T> {
  type: "update";
  entity: string;      // Entity name
  id: string;          // Entity ID
  data: T;             // Current entity instance
  changedFields: Record<string, any>; // Only the changed fields
  depth: number;
}

DeleteOperation

Represents an entity that needs to be removed:
interface DeleteOperation<T> {
  type: "delete";
  entity: string;      // Entity name
  id: string;          // Entity ID
  data: T;             // The deleted entity (for reference)
  depth: number;
}

Query Methods

Checking for Changes

const changes = order.getChanges();

// Check specific operation types
changes.hasCreates();  // true if any creates
changes.hasUpdates();  // true if any updates
changes.hasDeletes();  // true if any deletes

// Check overall state
changes.hasChanges();  // true if any operations exist
changes.isEmpty();     // true if no operations

// Get count
changes.count;  // total number of operations

Getting Operations

// Get all creates (sorted by depth ASC - root first)
const creates = changes.creates();
// CreateOperation[]

// Get all updates (no specific order)
const updates = changes.updates();
// UpdateOperation[]

// Get all deletes (sorted by depth DESC - leaf first)
const deletes = changes.deletes();
// DeleteOperation[]

Operation Ordering

The library automatically orders operations to respect foreign key constraints:

Deletes: Leaf → Root

Children must be deleted before parents to avoid FK violations:
// Given this structure:
// Order
// └── OrderItem
//     └── ItemDiscount

const deletes = changes.deletes();
// Returns in order:
// 1. ItemDiscount (depth: 2)
// 2. OrderItem (depth: 1)
// 3. Order (depth: 0)

Creates: Root → Leaf

Parents must be created before children so FKs can reference them:
const creates = changes.creates();
// Returns in order:
// 1. Order (depth: 0)
// 2. OrderItem (depth: 1)
// 3. ItemDiscount (depth: 2)

Updates: Any Order

Updates typically don’t affect relationships, so order doesn’t matter:
const updates = changes.updates();
// Order not guaranteed - apply in any order

Iteration

Operations Generator

Iterate over all operations in execution order:
// Order: deletes → creates → updates
for (const op of changes.operations()) {
  switch (op.type) {
    case "delete":
      console.log(`Delete ${op.entity} ${op.id}`);
      break;
    case "create":
      console.log(`Create ${op.entity}`);
      break;
    case "update":
      console.log(`Update ${op.entity} ${op.id}`, op.changedFields);
      break;
  }
}

Convert to Array

const allOps = changes.toArray();
// Operation[] in execution order (deletes, creates, updates)

Filtering by Entity

Use for() to get changes for a specific entity type:
const changes = order.getChanges();

const itemChanges = changes.for("OrderItem");

EntityChanges API

The for() method returns an EntityChanges object:
// Get entity arrays directly
itemChanges.creates;   // OrderItem[] - created entities
itemChanges.deletes;   // OrderItem[] - deleted entities

// Updates include the entity and changed fields
itemChanges.updates;
// Array<{ entity: OrderItem, changed: Record<string, any> }>

// Get IDs
itemChanges.createIds;  // string[] - IDs of created entities
itemChanges.updateIds;  // string[] - IDs of updated entities
itemChanges.deleteIds;  // string[] - IDs of deleted entities

// Check for changes
itemChanges.hasCreates();
itemChanges.hasUpdates();
itemChanges.hasDeletes();
itemChanges.hasChanges();
itemChanges.isEmpty();

// Count
itemChanges.count;

Example: Processing by Entity

const changes = order.getChanges();

// Process order updates
const orderChanges = changes.for("Order");
if (orderChanges.hasUpdates()) {
  const { entity, changed } = orderChanges.updates[0];
  await db.orders.update({
    where: { id: entity.id.value },
    data: changed,
  });
}

// Process item creates
const itemChanges = changes.for("OrderItem");
if (itemChanges.hasCreates()) {
  await db.orderItems.createMany({
    data: itemChanges.creates.map((item) => ({
      id: item.id.value,
      orderId: order.id.value,
      productId: item.productId,
      quantity: item.quantity,
      unitPrice: item.unitPrice,
    })),
  });
}

// Process item deletes
if (itemChanges.hasDeletes()) {
  await db.orderItems.deleteMany({
    where: {
      id: { in: itemChanges.deleteIds },
    },
  });
}

Batch Operations

The toBatchOperations() method groups operations by entity for efficient bulk database operations:
const batch = changes.toBatchOperations();

BatchOperations Structure

interface BatchOperations {
  deletes: Array<{
    entity: string;
    depth: number;
    ids: string[];
  }>;

  creates: Array<{
    entity: string;
    depth: number;
    items: Array<{
      data: any;
      parentId?: string;
    }>;
  }>;

  updates: Array<{
    entity: string;
    items: Array<{
      id: string;
      changedFields: Record<string, any>;
    }>;
  }>;
}

Using Batch Operations

const batch = changes.toBatchOperations();

// Process deletes (already sorted by depth DESC)
for (const del of batch.deletes) {
  console.log(`Deleting ${del.ids.length} ${del.entity}(s) at depth ${del.depth}`);
  
  await db[del.entity].deleteMany({
    where: { id: { in: del.ids } },
  });
}

// Process creates (already sorted by depth ASC)
for (const create of batch.creates) {
  console.log(`Creating ${create.items.length} ${create.entity}(s) at depth ${create.depth}`);
  
  await db[create.entity].createMany({
    data: create.items.map((item) => ({
      ...mapToPersistence(item.data),
      parentId: item.parentId,
    })),
  });
}

// Process updates (grouped by entity)
for (const update of batch.updates) {
  console.log(`Updating ${update.items.length} ${update.entity}(s)`);
  
  for (const item of update.items) {
    await db[update.entity].update({
      where: { id: item.id },
      data: item.changedFields,
    });
  }
}

Batch Operations with Prisma

async function persistChanges(order: Order, prisma: PrismaClient) {
  const changes = order.getChanges();
  const batch = changes.toBatchOperations();

  await prisma.$transaction(async (tx) => {
    // Deletes
    for (const del of batch.deletes) {
      switch (del.entity) {
        case "OrderItem":
          await tx.orderItem.deleteMany({
            where: { id: { in: del.ids } },
          });
          break;
        case "ItemDiscount":
          await tx.itemDiscount.deleteMany({
            where: { id: { in: del.ids } },
          });
          break;
      }
    }

    // Creates
    for (const create of batch.creates) {
      switch (create.entity) {
        case "OrderItem":
          await tx.orderItem.createMany({
            data: create.items.map((item) => ({
              id: item.data.id.value,
              orderId: item.parentId,
              productId: item.data.productId,
              quantity: item.data.quantity,
              unitPrice: item.data.unitPrice,
            })),
          });
          break;
        case "ItemDiscount":
          await tx.itemDiscount.createMany({
            data: create.items.map((item) => ({
              id: item.data.id.value,
              orderItemId: item.parentId,
              percentage: item.data.percentage,
            })),
          });
          break;
      }
    }

    // Updates
    for (const update of batch.updates) {
      switch (update.entity) {
        case "Order":
          for (const item of update.items) {
            await tx.order.update({
              where: { id: item.id },
              data: item.changedFields,
            });
          }
          break;
        case "OrderItem":
          for (const item of update.items) {
            await tx.orderItem.update({
              where: { id: item.id },
              data: item.changedFields,
            });
          }
          break;
      }
    }
  });

  order.markAsClean();
}

Type-Safe Usage

Defining Entity Map

type OrderEntities = {
  Order: Order;
  OrderItem: OrderItem;
  ItemDiscount: ItemDiscount;
  ShippingAddress: Address;
};

Using with getChanges

const changes = order.getChanges<OrderEntities>();

// 'for' now has autocomplete
const itemChanges = changes.for("OrderItem");
//                              ^? "Order" | "OrderItem" | "ItemDiscount" | "ShippingAddress"

// Returns are typed
itemChanges.creates.forEach((item) => {
  // item is OrderItem
  console.log(item.quantity);
});

itemChanges.updates.forEach(({ entity, changed }) => {
  // entity is OrderItem
  console.log(entity.id.value, changed);
});

Helper Method Pattern

class Order extends Aggregate<OrderProps> {
  getTypedChanges() {
    type Entities = {
      Order: Order;
      OrderItem: OrderItem;
      ItemDiscount: ItemDiscount;
    };
    return this.getChanges<Entities>();
  }
}

// Usage
const changes = order.getTypedChanges();
const items = changes.for("OrderItem"); // Fully typed!

Utility Methods

Get Affected Entities

const entities = changes.getAffectedEntities();
// ["Order", "OrderItem", "ItemDiscount"]

Clone

Create an independent copy:
const clone = changes.clone();
clone.clear(); // Doesn't affect original

console.log(changes.count);  // Still has operations
console.log(clone.count);    // 0

Clear

Remove all operations:
changes.clear();
console.log(changes.isEmpty()); // true

Raw Operations

Access underlying operations array (for debugging):
const raw = changes.rawOperations;
// Operation[] - unordered, all operations

Complete Example

class OrderService {
  constructor(
    private prisma: PrismaClient,
    private orderRepo: OrderRepository
  ) {}

  async confirmOrder(orderId: string): Promise<void> {
    // Load order
    const order = await this.orderRepo.findById(orderId);
    if (!order) throw new Error("Order not found");

    // Business logic
    order.confirm();
    order.items.forEach((item) => {
      if (item.quantity > 10) {
        item.applyBulkDiscount(5);
      }
    });

    // Get changes
    const changes = order.getTypedChanges();

    if (changes.isEmpty()) {
      return; // Nothing to persist
    }

    // Log what's changing
    console.log("Affected entities:", changes.getAffectedEntities());
    console.log("Total operations:", changes.count);

    // Persist
    const batch = changes.toBatchOperations();

    await this.prisma.$transaction(async (tx) => {
      // Process in correct order
      await this.processDeletes(tx, batch.deletes);
      await this.processCreates(tx, batch.creates);
      await this.processUpdates(tx, batch.updates);
    });

    // Clear change history
    order.markAsClean();
  }

  private async processDeletes(tx: any, deletes: BatchOperations["deletes"]) {
    for (const del of deletes) {
      await tx[this.getTableName(del.entity)].deleteMany({
        where: { id: { in: del.ids } },
      });
    }
  }

  private async processCreates(tx: any, creates: BatchOperations["creates"]) {
    for (const create of creates) {
      const table = this.getTableName(create.entity);
      await tx[table].createMany({
        data: create.items.map((item) =>
          this.mapToDatabase(create.entity, item.data, item.parentId)
        ),
      });
    }
  }

  private async processUpdates(tx: any, updates: BatchOperations["updates"]) {
    for (const update of updates) {
      const table = this.getTableName(update.entity);
      for (const item of update.items) {
        await tx[table].update({
          where: { id: item.id },
          data: item.changedFields,
        });
      }
    }
  }

  private getTableName(entity: string): string {
    const map: Record<string, string> = {
      Order: "order",
      OrderItem: "orderItem",
      ItemDiscount: "itemDiscount",
    };
    return map[entity];
  }

  private mapToDatabase(entity: string, data: any, parentId?: string): any {
    // Map domain entity to database record
    // ... implementation
  }
}

API Reference

AggregateChanges

MethodReturnsDescription
creates()CreateOperation[]All creates, sorted by depth ASC
updates()UpdateOperation[]All updates
deletes()DeleteOperation[]All deletes, sorted by depth DESC
operations()Generator<Operation>Iterator in execution order
toArray()Operation[]Array in execution order
for(entity)EntityChanges<T>Filter by entity name
toBatchOperations()BatchOperationsGroup for bulk operations
hasCreates()booleanHas any creates
hasUpdates()booleanHas any updates
hasDeletes()booleanHas any deletes
hasChanges()booleanHas any operations
isEmpty()booleanNo operations
getAffectedEntities()string[]List of entity names
clone()AggregateChangesCreate a copy
clear()voidRemove all operations

EntityChanges

Property/MethodReturnsDescription
createsT[]Created entities
updatesArray<{entity: T, changed: Record}>Updated entities with changes
deletesT[]Deleted entities
createIdsstring[]IDs of created entities
updateIdsstring[]IDs of updated entities
deleteIdsstring[]IDs of deleted entities
hasCreates()booleanHas any creates
hasUpdates()booleanHas any updates
hasDeletes()booleanHas any deletes
hasChanges()booleanHas any operations
isEmpty()booleanNo operations
countnumberTotal operations