ProofKit

Batch Operations

Batch operations allow you to execute multiple queries and operations together in a single request. All operations in a batch are executed atomically - they all succeed or all fail together. This is both more efficient (fewer network round-trips) and ensures data consistency across related operations.

Batch Result Structure

Batch operations return a BatchResult object that contains individual results for each operation. Each result has its own data, error, and status properties, allowing you to handle success and failure on a per-operation basis:

type BatchItemResult<T> = {
  data: T | undefined;
  error: FMODataErrorType | undefined;
  status: number; // HTTP status code (0 for truncated operations)
};

type BatchResult<T extends readonly any[]> = {
  results: { [K in keyof T]: BatchItemResult<T[K]> };
  successCount: number;
  errorCount: number;
  truncated: boolean; // true if FileMaker stopped processing due to an error
  firstErrorIndex: number | null; // Index of the first operation that failed
};

Basic Batch with Multiple Queries

Execute multiple read operations in a single batch:

// Create query builders
const contactsQuery = db.from(contacts).list().top(5);
const usersQuery = db.from(users).list().top(5);

// Execute both queries in a single batch
const result = await db.batch([contactsQuery, usersQuery]).execute();

// Access individual results
const [r1, r2] = result.results;

if (r1.error) {
  console.error("Contacts query failed:", r1.error);
} else {
  console.log("Contacts:", r1.data);
}

if (r2.error) {
  console.error("Users query failed:", r2.error);
} else {
  console.log("Users:", r2.data);
}

// Check summary statistics
console.log(`Success: ${result.successCount}, Errors: ${result.errorCount}`);

Mixed Operations (Reads and Writes)

Combine queries, inserts, updates, and deletes in a single batch:

// Mix different operation types
const listQuery = db.from(contacts).list().top(10);
const insertOp = db.from(contacts).insert({
  name: "John Doe",
  email: "john@example.com",
});
const updateOp = db.from(users).update({ active: true }).byId("user-123");

// All operations execute atomically
const result = await db.batch([listQuery, insertOp, updateOp]).execute();

// Access individual results
const [r1, r2, r3] = result.results;

if (r1.error) {
  console.error("List query failed:", r1.error);
} else {
  console.log("Fetched contacts:", r1.data);
}

if (r2.error) {
  console.error("Insert failed:", r2.error);
} else {
  console.log("Inserted contact:", r2.data);
}

if (r3.error) {
  console.error("Update failed:", r3.error);
} else {
  console.log("Updated user:", r3.data);
}

Handling Errors in Batches

When FileMaker encounters an error in a batch operation, it stops processing subsequent operations. Operations that were never executed due to an earlier error will have a BatchTruncatedError:

import { BatchTruncatedError, isBatchTruncatedError } from "@proofkit/fmodata";

const result = await db.batch([query1, query2, query3]).execute();

const [r1, r2, r3] = result.results;

// First operation succeeded
if (r1.error) {
  console.error("First query failed:", r1.error);
} else {
  console.log("First query succeeded:", r1.data);
}

// Second operation failed
if (r2.error) {
  console.error("Second query failed:", r2.error);
  console.log("HTTP Status:", r2.status); // e.g., 404
}

// Third operation was never executed (truncated)
if (r3.error && isBatchTruncatedError(r3.error)) {
  console.log("Third operation was not executed");
  console.log(`Failed at operation ${r3.error.failedAtIndex}`);
  console.log(`This operation index: ${r3.error.operationIndex}`);
  console.log("Status:", r3.status); // 0 (never executed)
}

// Check if batch was truncated
if (result.truncated) {
  console.log(`Batch stopped early at index ${result.firstErrorIndex}`);
}

Transactional Behavior

Batch operations are transactional for write operations (inserts, updates, deletes). If any operation in the batch fails, all write operations are rolled back:

const result = await db
  .batch([
    db.from(users).insert({ username: "alice", email: "alice@example.com" }),
    db.from(users).insert({ username: "bob", email: "bob@example.com" }),
    db.from(users).insert({ username: "charlie", email: "invalid" }), // This fails
  ])
  .execute();

// Check individual results
const [r1, r2, r3] = result.results;

if (r1.error || r2.error || r3.error) {
  // All three inserts are rolled back - no users were created
  console.error("Batch had errors:");
  if (r1.error) console.error("Operation 1:", r1.error);
  if (r2.error) console.error("Operation 2:", r2.error);
  if (r3.error) console.error("Operation 3:", r3.error);
}

Important Notes

  • FileMaker stops on first error: When an error occurs, FileMaker stops processing subsequent operations in the batch. Truncated operations will have BatchTruncatedError with status: 0.
  • Insert operations in batches: FileMaker ignores Prefer: return=representation in batch requests. Insert operations return {} or { ROWID?: number } instead of the full created record.
  • All results are always defined: Every operation in the batch will have a corresponding result in result.results, even if it was never executed (truncated operations).
  • Summary statistics: Use result.successCount, result.errorCount, result.truncated, and result.firstErrorIndex for quick batch status checks.

Batch operations automatically group write operations (POST, PATCH, DELETE) into changesets for transactional behavior, while read operations (GET) are executed individually within the batch.

On this page