API Records - Elixir SDK Documentation
Overview
The Records API provides comprehensive CRUD (Create, Read, Update, Delete) operations for collection records, along with powerful search, filtering, and authentication capabilities.
Key Features:
- Paginated list and search with filtering and sorting
- Single record retrieval with expand support
- Create, update, and delete operations
- Batch operations for multiple records
- Authentication methods (password, OAuth2, OTP)
- Email verification and password reset
- Relation expansion up to 6 levels deep
- Field selection and excerpt modifiers
Backend Endpoints:
GET /api/collections/{collection}/records- List recordsGET /api/collections/{collection}/records/{id}- View recordPOST /api/collections/{collection}/records- Create recordPATCH /api/collections/{collection}/records/{id}- Update recordDELETE /api/collections/{collection}/records/{id}- Delete recordPOST /api/batch- Batch operations
CRUD Operations
List/Search Records
Returns a paginated records list with support for sorting, filtering, and expansion.
alias Bosbase.{Client, RecordService}
client = Bosbase.new("http://127.0.0.1:8090")
posts = Client.collection(client, "posts")
# Basic list with pagination
{:ok, result} = RecordService.get_list(posts, %{
page: 1,
per_page: 50
})
IO.inspect(result["page"]) # 1
IO.inspect(result["perPage"]) # 50
IO.inspect(result["totalItems"]) # 150
IO.inspect(result["totalPages"]) # 3
IO.inspect(result["items"]) # List of records
Advanced List with Filtering and Sorting
# Filter and sort
{:ok, result} = RecordService.get_list(posts, %{
page: 1,
per_page: 50,
filter: ~s(created >= "2022-01-01 00:00:00" && status = "published"),
sort: "-created,title", # DESC by created, ASC by title
expand: "author,categories"
})
# Filter with operators
{:ok, result2} = RecordService.get_list(posts, %{
page: 1,
per_page: 50,
filter: ~s(title ~ "elixir" && views > 100),
sort: "-views"
})
Get Full List
Fetch all records at once (useful for small collections):
# Get all records
{:ok, all_posts} = RecordService.get_full_list(posts, 200, %{
sort: "-created",
filter: ~s(status = "published")
})
Get First Matching Record
Get only the first record that matches a filter:
{:ok, post} = RecordService.get_first_list_item(posts, ~s(slug = "my-post-slug"), %{
expand: "author,categories.tags"
})
View Record
Retrieve a single record by ID:
# Basic retrieval
{:ok, record} = RecordService.get_one(posts, "RECORD_ID")
# With expanded relations
{:ok, record} = RecordService.get_one(posts, "RECORD_ID", %{
expand: "author,categories,tags"
})
# Nested expand
comments = Client.collection(client, "comments")
{:ok, record} = RecordService.get_one(comments, "COMMENT_ID", %{
expand: "post.author,user"
})
# Field selection
{:ok, record} = RecordService.get_one(posts, "RECORD_ID", %{
fields: "id,title,content,author.name"
})
Create Record
Create a new record:
# Simple create
{:ok, record} = RecordService.create(posts, %{
body: %{
"title" => "My First Post",
"content" => "Lorem ipsum...",
"status" => "draft"
}
})
# Create with relations
{:ok, record} = RecordService.create(posts, %{
body: %{
"title" => "My Post",
"author" => "AUTHOR_ID", # Single relation
"categories" => ["cat1", "cat2"] # Multiple relation
}
})
# Create with file upload
{:ok, record} = RecordService.create(posts, %{
body: %{
"title" => "My Post"
},
files: %{
"image" => %Bosbase.FileParam{
content: File.read!("path/to/image.jpg"),
filename: "image.jpg",
content_type: "image/jpeg"
}
}
})
# Create with expand to get related data immediately
{:ok, record} = RecordService.create(posts, %{
body: %{
"title" => "My Post",
"author" => "AUTHOR_ID"
},
expand: "author"
})
Update Record
Update an existing record:
# Simple update
{:ok, record} = RecordService.update(posts, "RECORD_ID", %{
body: %{
"title" => "Updated Title",
"status" => "published"
}
})
# Update with relations
{:ok, _} = RecordService.update(posts, "RECORD_ID", %{
body: %{
"categories+" => "NEW_CATEGORY_ID", # Append
"tags-" => "OLD_TAG_ID" # Remove
}
})
# Update with file upload
{:ok, record} = RecordService.update(posts, "RECORD_ID", %{
body: %{
"title" => "Updated Title"
},
files: %{
"image" => %Bosbase.FileParam{
content: File.read!("path/to/new_image.jpg"),
filename: "new_image.jpg",
content_type: "image/jpeg"
}
}
})
# Update with expand
{:ok, record} = RecordService.update(posts, "RECORD_ID", %{
body: %{
"title" => "Updated"
},
expand: "author,categories"
})
Delete Record
Delete a record:
# Simple delete
:ok = RecordService.delete(posts, "RECORD_ID")
# Note: Returns :ok on success
# Returns {:error, error} if record doesn't exist or permission denied
Filter Syntax
The filter parameter supports a powerful query syntax:
Comparison Operators
# Equal
filter: ~s(status = "published")
# Not equal
filter: ~s(status != "draft")
# Greater than / Less than
filter: ~s(views > 100)
filter: ~s(created < "2023-01-01")
# Greater/Less than or equal
filter: ~s(age >= 18)
filter: ~s(price <= 99.99)
String Operators
# Contains (like)
filter: ~s(title ~ "elixir")
# Equivalent to: title LIKE "%elixir%"
# Not contains
filter: ~s(title !~ "deprecated")
# Exact match (case-sensitive)
filter: ~s(email = "user@example.com")
Array Operators (for multiple relations/files)
# Any of / At least one
filter: ~s(tags.id ?= "TAG_ID") # Any tag matches
filter: ~s(tags.name ?~ "important") # Any tag name contains "important"
# All must match
filter: ~s(tags.id = "TAG_ID" && tags.id = "TAG_ID2")
Logical Operators
# AND
filter: ~s(status = "published" && views > 100)
# OR
filter: ~s(status = "published" || status = "featured")
# Parentheses for grouping
filter: ~s((status = "published" || featured = true) && views > 50)
Sorting
Sort records using the sort parameter:
# Single field (ASC)
sort: "created"
# Single field (DESC)
sort: "-created"
# Multiple fields
sort: "-created,title" # DESC by created, then ASC by title
# Supported fields
sort: "@random" # Random order
sort: "@rowid" # Internal row ID
sort: "id" # Record ID
sort: "fieldName" # Any collection field
# Relation field sorting
sort: "author.name" # Sort by related author's name
Field Selection
Control which fields are returned:
# Specific fields
fields: "id,title,content"
# All fields at level
fields: "*"
# Nested field selection
fields: "*,author.name,author.email"
# Excerpt modifier for text fields
fields: "*,content:excerpt(200,true)"
# Returns first 200 characters with ellipsis if truncated
# Combined
fields: "*,content:excerpt(200),author.name,author.email"
Expanding Relations
Expand related records without additional API calls:
# Single relation
expand: "author"
# Multiple relations
expand: "author,categories,tags"
# Nested relations (up to 6 levels)
expand: "author.profile,categories.tags"
# Back-relations
expand: "comments_via_post.user"
See Relations Documentation for detailed information.
Pagination Options
# Skip total count (faster queries)
{:ok, result} = RecordService.get_list(posts, %{
page: 1,
per_page: 50,
skip_total: true, # totalItems and totalPages will be -1
filter: ~s(status = "published")
})
# Get Full List with batch processing
{:ok, all_posts} = RecordService.get_full_list(posts, 200, %{
sort: "-created"
})
# Processes in batches of 200 to avoid memory issues
Batch Operations
Execute multiple operations in a single transaction:
# Create a batch
batch = Client.create_batch(client)
# Add operations
Bosbase.BatchService.collection(batch, "posts")
|> Bosbase.BatchService.create(%{"title" => "Post 1", "author" => "AUTHOR_ID"})
Bosbase.BatchService.collection(batch, "posts")
|> Bosbase.BatchService.create(%{"title" => "Post 2", "author" => "AUTHOR_ID"})
Bosbase.BatchService.collection(batch, "tags")
|> Bosbase.BatchService.update("TAG_ID", %{"name" => "Updated Tag"})
Bosbase.BatchService.collection(batch, "categories")
|> Bosbase.BatchService.delete("CAT_ID")
# Send batch request
{:ok, results} = Bosbase.BatchService.send(batch)
# Results is a list matching the order of operations
Enum.each(results, fn result ->
if result["status"] >= 400 do
IO.inspect("Operation failed: #{inspect(result["body"])}")
else
IO.inspect("Operation succeeded: #{inspect(result["body"])}")
end
end)
Note: Batch operations must be enabled in Dashboard > Settings > Application.
Authentication Actions
List Auth Methods
Get available authentication methods for a collection:
{:ok, methods} = RecordService.list_auth_methods(users)
IO.inspect(methods["password"]["enabled"]) # true/false
IO.inspect(methods["oauth2"]["enabled"]) # true/false
IO.inspect(methods["oauth2"]["providers"]) # List of OAuth2 providers
IO.inspect(methods["otp"]["enabled"]) # true/false
IO.inspect(methods["mfa"]["enabled"]) # true/false
Auth with Password
{:ok, auth_data} = RecordService.auth_with_password(
users,
"user@example.com", # username or email
"password123"
)
# Auth data is automatically stored in client.auth_store
IO.inspect(Bosbase.AuthStore.valid?(client.auth_store)) # true
IO.inspect(Bosbase.AuthStore.token(client.auth_store)) # JWT token
IO.inspect(Bosbase.AuthStore.record(client.auth_store)) # User record
# With expand
{:ok, auth_data} = RecordService.auth_with_password(
users,
"user@example.com",
"password123",
%{"expand" => "profile"}
)
Auth with OAuth2
# Step 1: Get OAuth2 URL (usually done in UI)
{:ok, methods} = RecordService.list_auth_methods(users)
provider = Enum.find(methods["oauth2"]["providers"], fn p -> p["name"] == "google" end)
# Step 2: After redirect, exchange code for token
{:ok, auth_data} = RecordService.auth_with_oauth2_code(
users,
"google", # Provider name
"AUTHORIZATION_CODE", # From redirect URL
provider["codeVerifier"], # From step 1
"https://yourapp.com/callback", # Redirect URL
%{ # Optional data for new accounts
"name" => "John Doe"
}
)
Auth with OTP (One-Time Password)
# Step 1: Request OTP
{:ok, otp_request} = RecordService.request_otp(users, "user@example.com")
# Returns: %{"otpId" => "..."}
# Step 2: User enters OTP from email
# Step 3: Authenticate with OTP
{:ok, auth_data} = RecordService.auth_with_otp(
users,
otp_request["otpId"],
"123456" # OTP from email
)
Auth Refresh
Refresh the current auth token and get updated user data:
# Refresh auth (useful on page reload)
{:ok, auth_data} = RecordService.auth_refresh(users)
# Check if still valid
if Bosbase.AuthStore.valid?(client.auth_store) do
IO.puts("User is authenticated")
else
IO.puts("Token expired or invalid")
end
Email Verification
# Request verification email
:ok = RecordService.request_verification(users, "user@example.com")
# Confirm verification (on verification page)
:ok = RecordService.confirm_verification(users, "VERIFICATION_TOKEN")
Password Reset
# Request password reset email
:ok = RecordService.request_password_reset(users, "user@example.com")
# Confirm password reset (on reset page)
# Note: This invalidates all previous auth tokens
:ok = RecordService.confirm_password_reset(
users,
"RESET_TOKEN",
"newpassword123",
"newpassword123" # Confirm
)
Email Change
# Must be authenticated first
{:ok, _} = RecordService.auth_with_password(users, "user@example.com", "password")
# Request email change
:ok = RecordService.request_email_change(users, "newemail@example.com")
# Confirm email change (on confirmation page)
# Note: This invalidates all previous auth tokens
:ok = RecordService.confirm_email_change(
users,
"EMAIL_CHANGE_TOKEN",
"currentpassword"
)
Impersonate (Superuser Only)
Generate a token to authenticate as another user:
# Must be authenticated as superuser
admins = Client.collection(client, "_superusers")
{:ok, _} = RecordService.auth_with_password(admins, "admin@example.com", "password")
# Impersonate a user
{:ok, impersonate_client} = RecordService.impersonate(users, "USER_ID", 3600)
# Returns a new client instance with impersonated user's token
# Use the impersonate client
impersonate_posts = Client.collection(impersonate_client, "posts")
{:ok, posts} = RecordService.get_full_list(impersonate_posts)
# Access the token
IO.inspect(Bosbase.AuthStore.token(impersonate_client.auth_store))
IO.inspect(Bosbase.AuthStore.record(impersonate_client.auth_store))
Complete Examples
Example 1: Blog Post Search with Filters
defmodule PostSearch do
alias Bosbase.{Client, RecordService}
def search_posts(client, query, category_id, min_views) do
posts = Client.collection(client, "posts")
filter = ~s(title ~ "#{query}" || content ~ "#{query}")
filter = if category_id do
filter <> ~s( && categories.id ?= "#{category_id}")
else
filter
end
filter = if min_views do
filter <> ~s( && views >= #{min_views})
else
filter
end
{:ok, result} = RecordService.get_list(posts, %{
page: 1,
per_page: 20,
filter: filter,
sort: "-created",
expand: "author,categories"
})
result["items"]
end
end
# Usage
client = Bosbase.new("http://localhost:8090")
posts = PostSearch.search_posts(client, "elixir", "cat123", 100)
Example 2: User Dashboard with Related Content
defmodule UserDashboard do
alias Bosbase.{Client, RecordService}
def get_user_dashboard(client, user_id) do
posts = Client.collection(client, "posts")
comments = Client.collection(client, "comments")
# Get user's posts
{:ok, posts_result} = RecordService.get_list(posts, %{
page: 1,
per_page: 10,
filter: ~s(author = "#{user_id}"),
sort: "-created",
expand: "categories"
})
# Get user's comments
{:ok, comments_result} = RecordService.get_list(comments, %{
page: 1,
per_page: 10,
filter: ~s(user = "#{user_id}"),
sort: "-created",
expand: "post"
})
%{
posts: posts_result["items"],
comments: comments_result["items"]
}
end
end
Example 3: Advanced Filtering
{:ok, result} = RecordService.get_list(posts, %{
page: 1,
per_page: 50,
filter: ~s(
(status = "published" || featured = true) &&
created >= "2023-01-01" &&
(tags.id ?= "important" || categories.id = "news") &&
views > 100 &&
author.email != ""
),
sort: "-views,created",
expand: "author.profile,tags,categories",
fields: "*,content:excerpt(300),author.name,author.email"
})
Example 4: Batch Create Posts
defmodule BatchPosts do
alias Bosbase.{Client, BatchService}
def create_multiple_posts(client, posts_data) do
batch = Client.create_batch(client)
Enum.each(posts_data, fn post_data ->
BatchService.collection(batch, "posts")
|> BatchService.create(post_data)
end)
{:ok, results} = BatchService.send(batch)
# Check for failures
failures = results
|> Enum.with_index()
|> Enum.filter(fn {result, _index} -> result["status"] >= 400 end)
if length(failures) > 0 do
IO.inspect("Some posts failed to create: #{inspect(failures)}")
end
Enum.map(results, fn r -> r["body"] end)
end
end
Example 5: Pagination Helper
defmodule PaginationHelper do
alias Bosbase.{Client, RecordService}
def get_all_records_paginated(client, collection_name, opts \\ %{}) do
collection = Client.collection(client, collection_name)
all_records = []
page = 1
has_more = true
while has_more do
{:ok, result} = RecordService.get_list(collection, Map.merge(opts, %{
page: page,
per_page: 500,
skip_total: true # Skip count for performance
}))
items = result["items"] || []
all_records = all_records ++ items
has_more = length(items) == 500
page = page + 1
end
all_records
end
defp while(condition, fun) do
if condition do
fun.()
while(condition, fun)
end
end
end
Error Handling
case RecordService.create(posts, %{body: %{"title" => "My Post"}}) do
{:ok, record} ->
IO.inspect("Record created: #{inspect(record)}")
{:error, %{status: 400}} ->
# Validation error
IO.inspect("Validation errors")
{:error, %{status: 403}} ->
# Permission denied
IO.inspect("Access denied")
{:error, %{status: 404}} ->
# Not found
IO.inspect("Collection or record not found")
{:error, error} ->
IO.inspect("Unexpected error: #{inspect(error)}")
end
Best Practices
- Use Pagination: Always use pagination for large datasets
- Skip Total When Possible: Use
skip_total: truefor better performance when you don’t need counts - Batch Operations: Use batch for multiple operations to reduce round trips
- Field Selection: Only request fields you need to reduce payload size
- Expand Wisely: Only expand relations you actually use
- Filter Before Sort: Apply filters before sorting for better performance
- Cache Auth Tokens: Auth tokens are automatically stored in
auth_store, no need to manually cache - Handle Errors: Always handle authentication and permission errors gracefully
Related Documentation
- Collections - Collection configuration
- Relations - Working with relations
- API Rules and Filters - Filter syntax details
- Authentication - Detailed authentication guide
- Files - File uploads and handling