API Records - Swift 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.
import BosBase
let client = try BosBaseClient(baseURLString: "http://127.0.0.1:8090")
// Basic list with pagination
let result: ListResult<JSONRecord> = try await client
.collection("posts")
.getList(page: 1, perPage: 50)
print(result.page) // 1
print(result.perPage) // 50
print(result.totalItems) // 150
print(result.totalPages) // 3
print(result.items) // Array of records
Advanced List with Filtering and Sorting
// Filter and sort
let result: ListResult<JSONRecord> = try await client
.collection("posts")
.getList(
page: 1,
perPage: 50,
filter: "created >= \"2022-01-01 00:00:00\" && status = \"published\"",
sort: "-created,title", // DESC by created, ASC by title
expand: "author,categories"
)
// Filter with operators
let result2: ListResult<JSONRecord> = try await client
.collection("posts")
.getList(
page: 1,
perPage: 50,
filter: "title ~ \"javascript\" && views > 100",
sort: "-views"
)
Get Full List
Fetch all records at once (useful for small collections):
// Get all records
let allPosts: [JSONRecord] = try await client
.collection("posts")
.getFullList(
filter: "status = \"published\"",
sort: "-created"
)
// With batch size for large collections
let allPosts: [JSONRecord] = try await client
.collection("posts")
.getFullList(
batchSize: 200,
sort: "-created"
)
Get First Matching Record
Get only the first record that matches a filter:
let post: JSONRecord = try await client
.collection("posts")
.getFirstListItem(
filter: "slug = \"my-post-slug\"",
expand: "author,categories.tags"
)
View Record
Retrieve a single record by ID:
// Basic retrieval
let record: JSONRecord = try await client
.collection("posts")
.getOne("RECORD_ID")
// With expanded relations
let record: JSONRecord = try await client
.collection("posts")
.getOne("RECORD_ID", expand: "author,categories,tags")
// Nested expand
let record: JSONRecord = try await client
.collection("comments")
.getOne("COMMENT_ID", expand: "post.author,user")
// Field selection
let record: JSONRecord = try await client
.collection("posts")
.getOne("RECORD_ID", fields: "id,title,content,author.name")
Create Record
Create a new record:
// Simple create
let record: JSONRecord = try await client
.collection("posts")
.create(body: [
"title": AnyCodable("My First Post"),
"content": AnyCodable("Lorem ipsum..."),
"status": AnyCodable("draft")
])
// Create with relations
let record: JSONRecord = try await client
.collection("posts")
.create(body: [
"title": AnyCodable("My Post"),
"author": AnyCodable("AUTHOR_ID"), // Single relation
"categories": AnyCodable(["cat1", "cat2"]) // Multiple relation
])
// Create with file upload (multipart/form-data)
let formData = MultipartFormData()
formData.append(name: "title", value: "My Post")
formData.append(name: "image", fileData: imageData, fileName: "image.jpg", mimeType: "image/jpeg")
let record: JSONRecord = try await client
.collection("posts")
.create(body: .multipartData(formData))
// Create with expand to get related data immediately
let record: JSONRecord = try await client
.collection("posts")
.create(
body: [
"title": AnyCodable("My Post"),
"author": AnyCodable("AUTHOR_ID")
],
expand: "author"
)
Update Record
Update an existing record:
// Simple update
let record: JSONRecord = try await client
.collection("posts")
.update("RECORD_ID", body: [
"title": AnyCodable("Updated Title"),
"status": AnyCodable("published")
])
// Update with relations
try await client
.collection("posts")
.update("RECORD_ID", body: [
"categories+": AnyCodable("NEW_CATEGORY_ID"), // Append
"tags-": AnyCodable("OLD_TAG_ID") // Remove
])
// Update with file upload
let formData = MultipartFormData()
formData.append(name: "title", value: "Updated Title")
formData.append(name: "image", fileData: newFileData, fileName: "new.jpg", mimeType: "image/jpeg")
let record: JSONRecord = try await client
.collection("posts")
.update("RECORD_ID", body: .multipartData(formData))
// Update with expand
let record: JSONRecord = try await client
.collection("posts")
.update("RECORD_ID", body: [
"title": AnyCodable("Updated")
], expand: "author,categories")
Delete Record
Delete a record:
// Simple delete
try await client
.collection("posts")
.delete("RECORD_ID")
// Note: Returns true on success
// Throws error if record doesn't exist or permission denied
Filter Syntax
The filter parameter supports a powerful query syntax:
Comparison Operators
// Equal
filter: "status = \"published\""
// Not equal
filter: "status != \"draft\""
// Greater than / Less than
filter: "views > 100"
filter: "created < \"2023-01-01\""
// Greater/Less than or equal
filter: "age >= 18"
filter: "price <= 99.99"
String Operators
// Contains (like)
filter: "title ~ \"javascript\""
// Equivalent to: title LIKE "%javascript%"
// Not contains
filter: "title !~ \"deprecated\""
// Exact match (case-sensitive)
filter: "email = \"user@example.com\""
Array Operators (for multiple relations/files)
// Any of / At least one
filter: "tags.id ?= \"TAG_ID\"" // Any tag matches
filter: "tags.name ?~ \"important\"" // Any tag name contains "important"
// All must match
filter: "tags.id = \"TAG_ID\" && tags.id = \"TAG_ID2\""
Logical Operators
// AND
filter: "status = \"published\" && views > 100"
// OR
filter: "status = \"published\" || status = \"featured\""
// Parentheses for grouping
filter: "(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)
let result: ListResult<JSONRecord> = try await client
.collection("posts")
.getList(
page: 1,
perPage: 50,
skipTotal: true, // totalItems and totalPages will be -1
filter: "status = \"published\""
)
// Get Full List with batch processing
let allPosts: [JSONRecord] = try await client
.collection("posts")
.getFullList(
batchSize: 200,
sort: "-created"
)
// Processes in batches of 200 to avoid memory issues
Batch Operations
Execute multiple operations in a single transaction:
// Create a batch
let batch = client.createBatch()
// Add operations
try batch
.collection("posts")
.create(body: [
"title": AnyCodable("Post 1"),
"author": AnyCodable("AUTHOR_ID")
])
try batch
.collection("posts")
.create(body: [
"title": AnyCodable("Post 2"),
"author": AnyCodable("AUTHOR_ID")
])
try batch
.collection("tags")
.update("TAG_ID", body: [
"name": AnyCodable("Updated Tag")
])
try batch
.collection("categories")
.delete("CAT_ID")
// Send batch request
let results = try await batch.send()
// Results is an array matching the order of operations
for (index, result) in results.enumerated() {
if let status = result["status"]?.value as? Int, status >= 400 {
print("Operation \(index) failed: \(result)")
} else {
print("Operation \(index) succeeded: \(result)")
}
}
Note: Batch operations must be enabled in Dashboard > Settings > Application.
Authentication Actions
List Auth Methods
Get available authentication methods for a collection:
let methods: JSONRecord = try await client
.collection("users")
.listAuthMethods()
print(methods["password"]?["enabled"] ?? false) // true/false
print(methods["oauth2"]?["enabled"] ?? false) // true/false
print(methods["oauth2"]?["providers"] ?? []) // Array of OAuth2 providers
print(methods["otp"]?["enabled"] ?? false) // true/false
print(methods["mfa"]?["enabled"] ?? false) // true/false
Auth with Password
let authData: RecordAuthResponse<JSONRecord> = try await client
.collection("users")
.authWithPassword(
identity: "user@example.com", // username or email
password: "password123"
)
// Auth data is automatically stored in client.authStore
print(client.authStore.isValid()) // true
print(client.authStore.token ?? "") // JWT token
print(client.authStore.record?["id"] ?? "") // User ID
// Access the returned data
print(authData.token)
print(authData.record)
// With expand
let authData: RecordAuthResponse<JSONRecord> = try await client
.collection("users")
.authWithPassword(
identity: "user@example.com",
password: "password123",
expand: "profile"
)
Auth with OAuth2
// Step 1: Get OAuth2 URL (usually done in UI)
let methods: JSONRecord = try await client
.collection("users")
.listAuthMethods()
// Step 2: After redirect, exchange code for token
let authData: RecordAuthResponse<JSONRecord> = try await client
.collection("users")
.authWithOAuth2Code(
provider: "google", // Provider name
code: "AUTHORIZATION_CODE", // From redirect URL
codeVerifier: "CODE_VERIFIER", // From step 1
redirectURL: "https://yourapp.com/callback", // Redirect URL
createData: [ // Optional data for new accounts
"name": AnyCodable("John Doe")
]
)
Auth with OTP (One-Time Password)
// Step 1: Request OTP
let otpRequest: OTPResponse = try await client
.collection("users")
.requestOTP(email: "user@example.com")
// Returns: OTPResponse with otpId
// Step 2: User enters OTP from email
// Step 3: Authenticate with OTP
let authData: RecordAuthResponse<JSONRecord> = try await client
.collection("users")
.authWithOTP(
otpId: otpRequest.otpId,
password: "123456" // OTP from email
)
Auth Refresh
Refresh the current auth token and get updated user data:
// Refresh auth (useful on app launch)
let authData: RecordAuthResponse<JSONRecord> = try await client
.collection("users")
.authRefresh()
// Check if still valid
if client.authStore.isValid() {
print("User is authenticated")
} else {
print("Token expired or invalid")
}
Email Verification
// Request verification email
try await client
.collection("users")
.requestVerification(email: "user@example.com")
// Confirm verification (on verification page)
try await client
.collection("users")
.confirmVerification(token: "VERIFICATION_TOKEN")
Password Reset
// Request password reset email
try await client
.collection("users")
.requestPasswordReset(email: "user@example.com")
// Confirm password reset (on reset page)
// Note: This invalidates all previous auth tokens
try await client
.collection("users")
.confirmPasswordReset(
token: "RESET_TOKEN",
password: "newpassword123",
passwordConfirm: "newpassword123"
)
Email Change
// Must be authenticated first
try await client
.collection("users")
.authWithPassword(identity: "user@example.com", password: "password")
// Request email change
try await client
.collection("users")
.requestEmailChange(newEmail: "newemail@example.com")
// Confirm email change (on confirmation page)
// Note: This invalidates all previous auth tokens
try await client
.collection("users")
.confirmEmailChange(
token: "EMAIL_CHANGE_TOKEN",
password: "currentpassword"
)
Impersonate (Superuser Only)
Generate a token to authenticate as another user:
// Must be authenticated as superuser
try await client
.collection("_superusers")
.authWithPassword(identity: "admin@example.com", password: "password")
// Impersonate a user
let impersonateAuth: RecordAuthResponse<JSONRecord> = try await client
.collection("users")
.impersonate(recordId: "USER_ID", duration: 3600)
// Returns auth data with impersonated user's token
// Use the impersonated token
client.authStore.save(token: impersonateAuth.token, record: impersonateAuth.record)
// Access the token
print(client.authStore.token ?? "")
print(client.authStore.record ?? [:])
Complete Examples
Example 1: Blog Post Search with Filters
func searchPosts(query: String, categoryId: String?, minViews: Int?) async throws -> [JSONRecord] {
var filter = "title ~ \"\(query)\" || content ~ \"\(query)\""
if let categoryId {
filter += " && categories.id ?= \"\(categoryId)\""
}
if let minViews {
filter += " && views >= \(minViews)"
}
let result: ListResult<JSONRecord> = try await client
.collection("posts")
.getList(
page: 1,
perPage: 20,
filter: filter,
sort: "-created",
expand: "author,categories"
)
return result.items
}
Example 2: User Dashboard with Related Content
func getUserDashboard(userId: String) async throws -> [String: [JSONRecord]] {
// Get user's posts
let posts: ListResult<JSONRecord> = try await client
.collection("posts")
.getList(
page: 1,
perPage: 10,
filter: "author = \"\(userId)\"",
sort: "-created",
expand: "categories"
)
// Get user's comments
let comments: ListResult<JSONRecord> = try await client
.collection("comments")
.getList(
page: 1,
perPage: 10,
filter: "user = \"\(userId)\"",
sort: "-created",
expand: "post"
)
return [
"posts": posts.items,
"comments": comments.items
]
}
Example 3: Advanced Filtering
// Complex filter example
let result: ListResult<JSONRecord> = try await client
.collection("posts")
.getList(
page: 1,
perPage: 50,
filter: """
(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
func createMultiplePosts(postsData: [[String: AnyCodable]]) async throws -> [JSONRecord] {
let batch = client.createBatch()
for postData in postsData {
try batch
.collection("posts")
.create(body: postData)
}
let results = try await batch.send()
// Check for failures
let failures = results.enumerated().compactMap { index, result -> (Int, JSONRecord)? in
if let status = result["status"]?.value as? Int, status >= 400 {
return (index, result)
}
return nil
}
if !failures.isEmpty {
print("Some posts failed to create: \(failures)")
}
return results.compactMap { $0["body"]?.value as? JSONRecord }
}
Error Handling
do {
let record: JSONRecord = try await client
.collection("posts")
.create(body: [
"title": AnyCodable("My Post")
])
} catch let error as ClientResponseError {
if error.status == 400 {
// Validation error
print("Validation errors: \(error.response ?? [:])")
} else if error.status == 403 {
// Permission denied
print("Access denied")
} else if error.status == 404 {
// Not found
print("Collection or record not found")
} else {
print("Unexpected error: \(error)")
}
} catch {
print("Unexpected error: \(error)")
}
Best Practices
- Use Pagination: Always use pagination for large datasets
- Skip Total When Possible: Use
skipTotal: 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
authStore, 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