CRUD Operations
View SourceThis guide covers Create, Read, Update, and Delete operations using AshTypescript-generated RPC functions.
Overview
All CRUD operations follow a consistent pattern:
- Field selection using the
fieldsparameter - Type-safe input/output based on your Ash resources
- Explicit error handling with
{success: true/false}return values - Support for relationships and nested field selection
List/Read Operations
List Multiple Records
import { listTodos } from './ash_rpc';
const todos = await listTodos({
fields: ["id", "title", "completed", "priority"],
filter: { completed: { eq: false } },
sort: "-priority,+createdAt"
});
if (todos.success) {
console.log("Found todos:", todos.data);
}Get Single Record
import { getTodo } from './ash_rpc';
const todo = await getTodo({
fields: ["id", "title", "completed", "priority"],
input: { id: "todo-123" }
});
if (todo.success) {
console.log("Todo:", todo.data);
}Get by Specific Fields
Use get_by actions to lookup records by specific fields:
# Elixir configuration
rpc_action :get_user_by_email, :read, get_by: [:email]const user = await getUserByEmail({
getBy: { email: "user@example.com" },
fields: ["id", "name", "email"]
});Handling Not Found
Use not_found_error?: false to return null instead of an error:
# Elixir configuration
rpc_action :find_user, :read, get_by: [:email], not_found_error?: falseconst user = await findUser({
getBy: { email: "maybe@example.com" },
fields: ["id", "name"]
});
if (user.success) {
if (user.data) {
console.log("Found:", user.data.name);
} else {
console.log("User not found");
}
}With Relationships
Include related data using nested field selection:
const todo = await getTodo({
fields: [
"id",
"title",
{ user: ["name", "email"] }
],
input: { id: "todo-123" }
});
if (todo.success) {
console.log("Todo:", todo.data.title);
console.log("Created by:", todo.data.user.name);
}Calculated Fields
Request calculated fields computed by your Ash resource:
const todo = await getTodo({
fields: [
"id",
"title",
"dueDate",
"isOverdue", // Boolean calculation
"daysUntilDue" // Integer calculation
],
input: { id: "todo-123" }
});Create Operations
import { createTodo } from './ash_rpc';
const newTodo = await createTodo({
fields: ["id", "title", "createdAt"],
input: {
title: "Learn AshTypescript",
priority: "high",
dueDate: "2024-01-01",
userId: "user-id-123"
}
});
if (newTodo.success) {
console.log("Created todo:", newTodo.data);
} else {
console.error("Failed to create:", newTodo.errors);
}Update Operations
Update existing records using a separate identity parameter:
import { updateTodo } from './ash_rpc';
const updatedTodo = await updateTodo({
fields: ["id", "title", "priority", "updatedAt"],
identity: "todo-123", // Identity as separate parameter
input: {
title: "Updated: Learn AshTypescript",
priority: "urgent"
}
});Important: The identity parameter is separate from the input object. This ensures identity fields cannot be accidentally modified.
Update with Named Identities
Configure update actions to use named identities instead of the primary key:
# Elixir configuration
rpc_action :update_user_by_email, :update, identities: [:email]const updated = await updateUserByEmail({
identity: { email: "user@example.com" },
input: { name: "New Name" },
fields: ["id", "name"]
});See RPC Action Options for detailed identity configuration.
Delete Operations
import { destroyTodo } from './ash_rpc';
const deletedTodo = await destroyTodo({
identity: "todo-123"
});
if (deletedTodo.success) {
console.log("Todo deleted successfully");
}Delete with Named Identities
# Elixir configuration
rpc_action :destroy_user_by_email, :destroy, identities: [:email]await destroyUserByEmail({
identity: { email: "user@example.com" }
});Error Handling
All RPC functions return a {success: true/false} structure:
const result = await createTodo({
fields: ["id", "title"],
input: { title: "New Todo", userId: "user-id-123" }
});
if (result.success) {
console.log("Created:", result.data);
} else {
result.errors.forEach(error => {
console.error(`Error: ${error.message}`);
if (error.fields.length > 0) {
console.error(`Fields: ${error.fields.join(', ')}`);
}
});
}See Error Handling for comprehensive error handling strategies.
Authentication and Headers
All RPC functions accept optional headers:
import { listTodos, buildCSRFHeaders } from './ash_rpc';
// With CSRF protection
const todos = await listTodos({
fields: ["id", "title"],
headers: buildCSRFHeaders()
});
// With Bearer token
const todos = await listTodos({
fields: ["id", "title"],
headers: {
"Authorization": "Bearer your-token-here"
}
});
// Combined
const todos = await listTodos({
fields: ["id", "title"],
headers: {
...buildCSRFHeaders(),
"Authorization": "Bearer your-token-here"
}
});Complete Example
import {
listTodos,
getTodo,
createTodo,
updateTodo,
destroyTodo,
buildCSRFHeaders
} from './ash_rpc';
const headers = buildCSRFHeaders();
// 1. Create
const createResult = await createTodo({
fields: ["id", "title", "createdAt"],
input: { title: "Learn AshTypescript CRUD", priority: "high", userId: "user-123" },
headers
});
if (!createResult.success) return;
const todoId = createResult.data.id;
// 2. Read (single)
const getResult = await getTodo({
fields: ["id", "title", "priority", { user: ["name"] }],
input: { id: todoId },
headers
});
// 3. Read (list)
const listResult = await listTodos({
fields: ["id", "title", "completed"],
filter: { completed: { eq: false } },
headers
});
// 4. Update
const updateResult = await updateTodo({
fields: ["id", "title", "updatedAt"],
identity: todoId,
input: { title: "Mastered AshTypescript CRUD" },
headers
});
// 5. Delete
const deleteResult = await destroyTodo({
identity: todoId,
headers
});Next Steps
- Field Selection - Advanced field selection patterns
- Querying Data - Pagination, sorting, and filtering
- Error Handling - Comprehensive error handling
- RPC Action Options - Identity lookups, load restrictions
- Custom Fetch - Request customization and interceptors