Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/get-query-string-entity-ids-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@proofkit/fmodata": patch
---

Add `useEntityIds` override parameter to `getQueryString()` methods in QueryBuilder and RecordBuilder, allowing users to override entity ID usage when inspecting query strings without executing requests.

7 changes: 5 additions & 2 deletions apps/docs/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
"registries": {
"@reui": "https://reui.io/r/{name}.json"
}
}
171 changes: 171 additions & 0 deletions apps/docs/content/docs/fmodata/batch.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
title: Batch Operations
---

import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";

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:

```typescript
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:

```typescript
// 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:

```typescript
// 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`:

```typescript
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:

```typescript
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.

<Callout type="info">
Batch operations automatically group write operations (POST, PATCH, DELETE) into changesets for transactional behavior, while read operations (GET) are executed individually within the batch.
</Callout>
152 changes: 152 additions & 0 deletions apps/docs/content/docs/fmodata/crud.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
title: Modifying Data
---

import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";

## Insert

Insert new records with type-safe data:

```typescript
// Insert a new user
const result = await db
.from(users)
.insert({
username: "johndoe",
email: "john@example.com",
active: true,
})
.execute();

if (result.data) {
console.log("Created user:", result.data);
}
```

Fields are automatically required for insert if they use `.notNull()`. Read-only fields (including primary keys) are automatically excluded:

```typescript
const users = fmTableOccurrence("users", {
id: textField().primaryKey(), // Auto-required, but excluded from insert (primaryKey)
username: textField().notNull(), // Auto-required (notNull)
email: textField().notNull(), // Auto-required (notNull)
phone: textField(), // Optional by default (nullable)
createdAt: timestampField().readOnly(), // Excluded from insert/update
});

// TypeScript enforces: username and email are required
// TypeScript excludes: id and createdAt cannot be provided
const result = await db
.from(users)
.insert({
username: "johndoe",
email: "john@example.com",
phone: "+1234567890", // Optional
})
.execute();
```

## Update

Update records by ID or filter:

```typescript
// Update by ID
const result = await db
.from(users)
.update({ username: "newname" })
.byId("user-123")
.execute();

if (result.data) {
console.log(`Updated ${result.data.updatedCount} record(s)`);
}

// Update by filter (using ORM API)
import { lt, and, eq } from "@proofkit/fmodata";

const result = await db
.from(users)
.update({ active: false })
.where(lt(users.lastLogin, "2023-01-01"))
.execute();

// Complex filter example
const result = await db
.from(users)
.update({ active: false })
.where(and(eq(users.active, true), lt(users.count, 5)))
.execute();
```

<Callout type="info">
All fields are optional for updates (except read-only fields which are automatically excluded). TypeScript will enforce that you can only update fields that aren't marked as read-only.
</Callout>

## Delete

Delete records by ID or filter:

```typescript
// Delete by ID
const result = await db.from(users).delete().byId("user-123").execute();

if (result.data) {
console.log(`Deleted ${result.data.deletedCount} record(s)`);
}

// Delete by filter (using ORM API)
import { eq, and, lt } from "@proofkit/fmodata";

const result = await db
.from(users)
.delete()
.where(eq(users.active, false))
.execute();

// Delete with complex filters
const result = await db
.from(users)
.delete()
.where(and(eq(users.active, false), lt(users.lastLogin, "2023-01-01")))
.execute();
```

## Required and Read-Only Fields

The library automatically infers which fields are required based on field builder configuration:

- **Auto-inference:** Fields with `.notNull()` are automatically required for insert
- **Primary keys:** Fields with `.primaryKey()` are automatically read-only
- **Read-only fields:** Use `.readOnly()` to exclude fields from insert/update (e.g., timestamps, calculated fields)
- **Update flexibility:** All fields are optional for updates (except read-only fields)

```typescript
const users = fmTableOccurrence("users", {
id: textField().primaryKey(), // Auto-required, auto-readOnly (primaryKey)
username: textField().notNull(), // Auto-required (notNull)
email: textField().notNull(), // Auto-required (notNull)
status: textField(), // Optional (nullable by default)
createdAt: timestampField().readOnly(), // Read-only system field
updatedAt: timestampField(), // Optional (nullable)
});

// Insert: username and email are required
// Insert: id and createdAt are excluded (cannot be provided - read-only)
db.from(users).insert({
username: "john",
email: "john@example.com",
status: "active", // Optional
updatedAt: new Date().toISOString(), // Optional
});

// Update: all fields are optional except id and createdAt are excluded
db.from(users)
.update({
status: "active", // Optional
// id and createdAt cannot be modified (read-only)
})
.byId("user-123");
```
16 changes: 16 additions & 0 deletions apps/docs/content/docs/fmodata/custom-fetch-handlers.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: Custom Fetch Handlers
description: You can provide custom fetch handlers for testing or custom networking
---

```typescript
const customFetch = async (url, options) => {
console.log("Fetching:", url);
return fetch(url, options);
};

const result = await db.from("users").list().execute({
fetchHandler: customFetch,
});
```

Loading
Loading