Advanced Field Selection
View SourceThis guide covers advanced patterns for field selection in AshTypescript RPC functions, including nested relationships, calculations, and performance optimization.
Overview
Field selection in AshTypescript allows you to precisely specify which data you need from your Ash resources. This approach:
- Reduces payload size: Only requested fields are returned
- Improves performance: Ash only loads and processes requested data
- Provides type safety: TypeScript infers exact return types based on selected fields
- Supports nesting: Select fields from related resources and calculations
Basic Field Selection
Simple Fields
Select specific attribute fields:
import { getTodo } from './ash_rpc';
const todo = await getTodo({
fields: ["id", "title", "completed", "priority"],
input: { id: "todo-123" }
});
if (todo.success) {
// TypeScript knows exact shape:
// { id: string, title: string, completed: boolean, priority: string }
console.log(todo.data.title);
console.log(todo.data.priority);
}Selecting All Basic Fields
You can select all non-relationship fields:
// Select multiple fields explicitly
const todo = await getTodo({
fields: [
"id",
"title",
"description",
"completed",
"priority",
"dueDate",
"createdAt",
"updatedAt"
],
input: { id: "todo-123" }
});Note: There is no "select all" option. This is intentional to prevent over-fetching and ensure you're explicit about data requirements, which is needed for full type-safety.
Nested Field Selection
Simple Relationships
Select fields from related resources:
import { getTodo } from './ash_rpc';
// Get todo with user information
const todo = await getTodo({
fields: [
"id",
"title",
{ user: ["name", "email", "avatarUrl"] }
],
input: { id: "todo-123" }
});
if (todo.success) {
console.log("Todo:", todo.data.title);
console.log("Created by:", todo.data.user.name);
console.log("Email:", todo.data.user.email);
}Multiple Relationships
Select from multiple related resources in one request:
const todo = await getTodo({
fields: [
"id",
"title",
"description",
{
user: ["name", "email"],
assignee: ["name", "email"],
tags: ["name", "color"]
}
],
input: { id: "todo-123" }
});
if (todo.success) {
console.log("Created by:", todo.data.user.name);
console.log("Assigned to:", todo.data.assignee.name);
console.log("Tags:", todo.data.tags.map(t => t.name).join(", "));
}Deep Nesting
Select fields from nested relationships:
const todo = await getTodo({
fields: [
"id",
"title",
{
comments: [
"id",
"text",
"createdAt",
{
author: [
"name",
"email",
{
profile: ["bio", "avatarUrl"]
}
]
}
]
}
],
input: { id: "todo-123" }
});
if (todo.success) {
todo.data.comments.forEach(comment => {
console.log(`${comment.author.name}: ${comment.text}`);
console.log(`Bio: ${comment.author.profile.bio}`);
});
}Many-to-Many Relationships
Handle many-to-many relationships with join resources:
// Todo has many tags through todo_tags
const todo = await getTodo({
fields: [
"id",
"title",
{
tags: [
"id",
"name",
"color"
]
}
],
input: { id: "todo-123" }
});
if (todo.success) {
// Tags array is automatically flattened
console.log("Tags:", todo.data.tags);
}Calculations
Basic Calculations
Request calculated fields that are computed by your Ash resource:
const todo = await getTodo({
fields: [
"id",
"title",
"completionPercentage", // Calculated field
"timeRemaining" // Calculated field
],
input: { id: "todo-123" }
});
if (todo.success) {
console.log("Progress:", todo.data.completionPercentage);
console.log("Time remaining:", todo.data.timeRemaining);
}Calculations with Arguments
Pass arguments to calculation fields:
const todo = await getTodo({
fields: [
"id",
"title",
{
priorityScore: {
args: { multiplier: 2.5, includeSubtasks: true },
fields: ["score", "rank", "category"]
}
}
],
input: { id: "todo-123" }
});
if (todo.success) {
console.log("Priority score:", todo.data.priorityScore.score);
console.log("Rank:", todo.data.priorityScore.rank);
console.log("Category:", todo.data.priorityScore.category);
}Nested Calculations
Combine calculations with relationships:
const todo = await getTodo({
fields: [
"id",
"title",
{
user: [
"name",
"email",
{
activityScore: {
args: { days: 30 },
fields: ["score", "trend"]
}
}
]
}
],
input: { id: "todo-123" }
});
if (todo.success) {
console.log("User:", todo.data.user.name);
console.log("Activity:", todo.data.user.activityScore.score);
console.log("Trend:", todo.data.user.activityScore.trend);
}Embedded Resources
Basic Embedded Resources
Select fields from embedded resources:
const todo = await getTodo({
fields: [
"id",
"title",
{
settings: ["theme", "notifications", "timezone"]
}
],
input: { id: "todo-123" }
});
if (todo.success) {
console.log("Theme:", todo.data.settings.theme);
console.log("Notifications:", todo.data.settings.notifications);
}Embedded Arrays
Handle arrays of embedded resources:
const todo = await getTodo({
fields: [
"id",
"title",
{
attachments: [
"filename",
"size",
"url",
"mimeType"
]
}
],
input: { id: "todo-123" }
});
if (todo.success) {
todo.data.attachments.forEach(attachment => {
console.log(`${attachment.filename} (${attachment.size} bytes)`);
});
}Nested Embedded Resources
Embedded resources can contain other embedded resources:
const user = await getUser({
fields: [
"id",
"name",
{
preferences: [
"language",
"timezone",
{
notifications: [
"email",
"push",
"sms"
]
}
]
}
],
input: { id: "user-123" }
});
if (user.success) {
console.log("Language:", user.data.preferences.language);
console.log("Email notifications:", user.data.preferences.notifications.email);
}Union Types
Selecting Union Fields
For union type fields, you can select fields from specific union members:
const todo = await getTodo({
fields: [
"id",
"title",
{
content: [
"text", // Common field across union members
{
textContent: ["text", "formatting"], // Text-specific fields
imageContent: ["url", "caption"], // Image-specific fields
videoContent: ["url", "thumbnail"] // Video-specific fields
}
]
}
],
input: { id: "todo-123" }
});
if (todo.success) {
// TypeScript understands the union type
const content = todo.data.content;
if (content.textContext) {
console.log("Text:", content.text);
console.log("Formatting:", content.formatting);
} else if (content.imageContent) {
console.log("Image URL:", content.url);
console.log("Caption:", content.caption);
}
}Union with Relationships
Union members can have relationships:
const notification = await getNotification({
fields: [
"id",
"timestamp",
{
payload: [
{
commentNotification: [
"message",
{ comment: ["text", { author: ["name"] }] }
],
mentionNotification: [
"message",
{ mentionedBy: ["name", "avatarUrl"] }
]
}
]
}
],
input: { id: "notif-123" }
});Lazy Load Details
Request minimal fields initially, then fetch details when needed:
// List view: minimal fields
const todosList = await listTodos({
fields: ["id", "title", "completed"]
});
// Detail view: full fields when user selects a todo
async function showTodoDetails(todoId: string) {
const todoDetail = await getTodo({
fields: [
"id",
"title",
"description",
"completed",
"priority",
"dueDate",
{
user: ["name", "email", "avatarUrl"],
comments: ["id", "text", "createdAt", { author: ["name"] }],
tags: ["name", "color"]
}
],
input: { id: todoId }
});
if (todoDetail.success) {
displayDetailView(todoDetail.data);
}
}Conditional Field Selection
Select different fields based on context:
type ViewMode = "list" | "grid" | "detail";
function getTodoFields(mode: ViewMode): any[] {
const baseFields = ["id", "title", "completed"];
switch (mode) {
case "list":
return [
...baseFields,
{ user: ["name"] }
];
case "grid":
return [
...baseFields,
"priority",
{ tags: ["color"] }
];
case "detail":
return [
...baseFields,
"description",
"priority",
"dueDate",
"createdAt",
{
user: ["name", "email", "avatarUrl"],
comments: ["id", "text", { author: ["name"] }],
tags: ["name", "color"]
}
];
}
}
// Use based on context
const todos = await listTodos({
fields: getTodoFields("list")
});Advanced Patterns
Field Selection Builders
Create reusable field selection builders:
const TodoFields = {
basic: ["id", "title", "completed"] as const,
withUser: [
"id", "title", "completed",
{ user: ["name", "email"] }
] as const,
withDetails: [
"id", "title", "description", "completed", "priority",
{ user: ["name", "email", "avatarUrl"] },
{ tags: ["name", "color"] }
] as const,
full: [
"id", "title", "description", "completed", "priority",
"dueDate", "createdAt", "updatedAt",
{
user: ["name", "email", "avatarUrl"],
assignee: ["name", "email"],
comments: ["id", "text", "createdAt", { author: ["name"] }],
tags: ["name", "color", "description"]
}
] as const
};
// Usage
const todos = await listTodos({
fields: TodoFields.withUser
});Type-Safe Field Selection
Use TypeScript to ensure field selection correctness:
// Define available fields
type TodoField =
| "id"
| "title"
| "description"
| "completed"
| "priority"
| "dueDate";
type TodoRelation = "user" | "assignee" | "tags" | "comments";
// Type-safe field selector
function selectTodoFields<F extends TodoField, R extends TodoRelation>(
fields: F[],
relations?: Record<R, string[]>
) {
const selection: any[] = [...fields];
if (relations) {
const relationSelection = Object.entries(relations).reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {} as Record<string, string[]>);
selection.push(relationSelection);
}
return selection;
}
// Usage with type safety
const fields = selectTodoFields(
["id", "title", "completed"],
{
user: ["name", "email"],
tags: ["name", "color"]
}
);
const todos = await listTodos({ fields });Pagination with Consistent Fields
Use consistent field selection across paginated requests:
const fields = ["id", "title", "completed", { user: ["name"] }];
// First page
const page1 = await listTodos({
fields,
page: { limit: 20, offset: 0 }
});
// Next page
const page2 = await listTodos({
fields,
page: { limit: 20, offset: 20 }
});
// Consistent field selection ensures predictable data structureCommon Patterns
List + Detail Pattern
Minimal fields for lists, full fields for details:
// List view
async function fetchTodoList() {
return await listTodos({
fields: ["id", "title", "completed", "priority"]
});
}
// Detail view
async function fetchTodoDetail(id: string) {
return await getTodo({
fields: [
"id", "title", "description", "completed", "priority",
"dueDate", "createdAt", "updatedAt",
{
user: ["name", "email", "avatarUrl"],
comments: ["id", "text", "createdAt", { author: ["name"] }],
tags: ["name", "color"]
}
],
input: { id }
});
}Search Results Pattern
Include fields relevant to displaying search results:
async function fetchTodosForDisplay() {
return await listTodos({
fields: [
"id",
"title",
"description",
"completed",
{ user: ["name"] },
{ tags: ["name"] }
]
});
}Related Documentation
- Basic CRUD Operations - Learn about basic field selection in CRUD operations
- Error Handling - Handle errors in field selection
- Phoenix Channels - Field selection with channel-based actions
- Configuration - Configure field name mapping