CRUD Operations

View Source

This 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 fields parameter
  • 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?: false
const 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