Data Service CRUD Operations
Complete guide to Creating, Reading, Updating, and Deleting data in Taruvi Data Service tables.
Overview
The Data Service provides a complete CRUD (Create, Read, Update, Delete) API for manipulating data in your tables. These operations work on the actual data stored in materialized tables, as opposed to schema operations which manage table structures.
Base URL: /api/apps/{app_slug}/datatables/{name}/data/
Key Features:
- Single and bulk operations
- Upsert capabilities (insert or update)
- Advanced querying with filters, sorting, pagination
- Relationship population
- Soft delete support
- Automatic validation
Creating Data
Single Record Creation
Create a single record by sending a JSON object.
- REST API
- Python
- JavaScript
POST /api/apps/blog/datatables/posts/data/
Content-Type: application/json
{
"title": "My First Post",
"content": "This is the content of my post",
"author_id": 1,
"status": "draft"
}
Response (201 Created):
{
"data": {
"id": 42,
"title": "My First Post",
"content": "This is the content of my post",
"author_id": 1,
"status": "draft",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
# Create a single record
new_post = auth_client.database.create(
"posts",
data={
"title": "My First Post",
"content": "This is the content of my post",
"author_id": 1,
"status": "draft"
}
)
print(f"Created post with ID: {new_post['id']}")
print(f"Title: {new_post['title']}")
print(f"Status: {new_post['status']}")
import { Database } from '@taruvi/sdk'
const database = new Database(client)
// Create a single record
const newPost = await database.from("posts").insert({
title: "My First Post",
content: "This is the content of my post",
author_id: 1,
status: "draft"
}).execute()
console.log(`Created post with ID: ${newPost.data.id}`)
console.log(`Title: ${newPost.data.title}`)
console.log(`Status: ${newPost.data.status}`)
Bulk Insert
Create multiple records at once by sending an array.
- REST API
- Python
- JavaScript
POST /api/apps/blog/datatables/comments/data/
Content-Type: application/json
[
{
"post_id": 42,
"author": "Alice",
"content": "Great post!"
},
{
"post_id": 42,
"author": "Bob",
"content": "Thanks for sharing"
}
]
Response (201 Created):
{
"data": [
{
"id": 1,
"post_id": 42,
"author": "Alice",
"content": "Great post!",
"created_at": "2024-01-15T10:31:00Z"
},
{
"id": 2,
"post_id": 42,
"author": "Bob",
"content": "Thanks for sharing",
"created_at": "2024-01-15T10:31:00Z"
}
]
}
# Create multiple records at once
comments = auth_client.database.create(
"comments",
data=[
{
"post_id": 42,
"author": "Alice",
"content": "Great post!"
},
{
"post_id": 42,
"author": "Bob",
"content": "Thanks for sharing"
}
]
)
print(f"Created {len(comments)} comments")
for comment in comments:
print(f"- Comment {comment['id']} by {comment['author']}")
import { Database } from '@taruvi/sdk'
const database = new Database(client)
// Create multiple records at once
const comments = await database.from("comments").insert([
{
post_id: 42,
author: "Alice",
content: "Great post!"
},
{
post_id: 42,
author: "Bob",
content: "Thanks for sharing"
}
]).execute()
console.log(`Created ${comments.data.length} comments`)
comments.data.forEach(comment => {
console.log(`- Comment ${comment.id} by ${comment.author}`)
})
Upsert (Insert or Update)
Update existing records or create new ones based on unique fields:
- REST API
- Python
- JavaScript
POST /api/apps/blog/datatables/users/data/upsert/?unique_fields=email
Content-Type: application/json
{
"email": "alice@example.com",
"name": "Alice Smith",
"bio": "Software engineer"
}
If email exists - Updates the existing record If email doesn't exist - Creates a new record
# Note: Upsert is not directly supported in SDK
# Workaround: Check if record exists, then create or update
# Option 1: Try to get, then create or update
try:
existing_user = auth_client.database.query('users') \
.filter('email', 'eq', 'alice@example.com') \
.first()
if existing_user:
# Update existing record
user = auth_client.database.update('users', existing_user['id'], {
'name': 'Alice Smith',
'bio': 'Software engineer'
})
else:
# Create new record
user = auth_client.database.create('users', {
'email': 'alice@example.com',
'name': 'Alice Smith',
'bio': 'Software engineer'
})
except Exception as e:
# Create if not found
user = auth_client.database.create('users', {
'email': 'alice@example.com',
'name': 'Alice Smith',
'bio': 'Software engineer'
})
SDK Limitation: The SDK does not have a built-in upsert method. Use the REST API directly for true upsert functionality, or implement check-then-create/update logic as shown above.
// Note: Upsert is not directly supported in SDK
// Workaround: Check if record exists, then create or update
try {
const existingUser = await client.database
.from('users')
.filter('email', 'eq', 'alice@example.com')
.first()
if (existingUser) {
// Update existing record
const user = await client.database
.from('users')
.get(existingUser.id.toString())
.update({
name: 'Alice Smith',
bio: 'Software engineer'
})
.execute()
} else {
// Create new record
const user = await client.database
.from('users')
.create({
email: 'alice@example.com',
name: 'Alice Smith',
bio: 'Software engineer'
})
.execute()
}
} catch (error) {
// Create if not found
const user = await client.database
.from('users')
.create({
email: 'alice@example.com',
name: 'Alice Smith',
bio: 'Software engineer'
})
.execute()
}
SDK Limitation: The SDK does not have a built-in upsert method. Use the REST API directly for true upsert functionality, or implement check-then-create/update logic as shown above.
Response (200 OK or 201 Created):
{
"data": {
"id": 5,
"email": "alice@example.com",
"name": "Alice Smith",
"bio": "Software engineer",
"created_at": "2024-01-10T08:00:00Z",
"updated_at": "2024-01-15T10:32:00Z"
},
"created": false
}
Bulk Upsert:
POST /api/apps/blog/datatables/users/data/upsert/?unique_fields=email
Content-Type: application/json
[
{"email": "alice@example.com", "name": "Alice"},
{"email": "bob@example.com", "name": "Bob"}
]
Validation
All create operations validate data against the table schema:
- Required fields: Must be present
- Type checking: Values must match field types (string, integer, etc.)
- Constraints: Min/max length, pattern matching, enum values
- Foreign keys: Referenced IDs must exist
- Unique constraints: Duplicate values rejected
Validation Error Example (400 Bad Request):
{
"error": "Validation failed",
"details": {
"title": ["This field is required"],
"author_id": ["Foreign key constraint violated: author with id=999 does not exist"],
"status": ["Value must be one of: draft, published, archived"]
}
}
Reading Data
List All Records
Fetch all records with pagination.
- REST API
- Python
- JavaScript
GET /api/apps/blog/datatables/posts/data/
Response (200 OK):
{
"data": [
{
"id": 1,
"title": "First Post",
"author_id": 1,
"created_at": "2024-01-01T12:00:00Z"
},
{
"id": 2,
"title": "Second Post",
"author_id": 2,
"created_at": "2024-01-02T12:00:00Z"
}
],
"total": 50,
"page": 1,
"page_size": 20
}
# Get all records with pagination
posts = auth_client.database.query("posts").page_size(20).get()
print(f"Total posts: {posts.get('total', 0)}")
print(f"Current page: {posts.get('page', 1)}")
for post in posts['data']:
print(f"Post {post['id']}: {post['title']}")
import { Database } from '@taruvi/sdk'
const database = new Database(client)
// Get all records with pagination
const posts = await database.from("posts")
.limit(20)
.execute()
console.log(`Total posts: ${posts.total || 0}`)
console.log(`Current page: ${posts.page || 1}`)
posts.data.forEach(post => {
console.log(`Post ${post.id}: ${post.title}`)
})
Get Single Record
Fetch a specific record by ID.
- REST API
- Python
- JavaScript
GET /api/apps/blog/datatables/posts/data/42/
Response (200 OK):
{
"data": {
"id": 42,
"title": "My First Post",
"content": "This is the content",
"author_id": 1,
"status": "published",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T11:00:00Z"
}
}
Not Found (404):
{
"error": "Record not found",
"detail": "No post with id=999"
}
# Get a single record by ID
post = auth_client.database.get("posts", record_id=42)
print(f"Post: {post['title']}")
print(f"Status: {post['status']}")
print(f"Author ID: {post['author_id']}")
# Handle not found
try:
post = auth_client.database.get("posts", record_id=999)
except Exception as e:
print(f"Error: {e}")
import { Database } from '@taruvi/sdk'
const database = new Database(client)
try {
// Get a single record by ID
const post = await database.from("posts").get(42)
console.log(`Post: ${post.data.title}`)
console.log(`Status: ${post.data.status}`)
console.log(`Author ID: ${post.data.author_id}`)
} catch (error) {
console.error(`Error: ${error.message}`)
}
Pagination
Page-based pagination:
GET /api/apps/blog/datatables/posts/data/?page=2&page_size=10
Offset-based pagination:
GET /api/apps/blog/datatables/posts/data/?limit=10&offset=20
Filtering
Apply filters using query parameters. See Querying Guide for complete filter operators.
GET /api/apps/blog/datatables/posts/data/?status=published&author_id=1
GET /api/apps/blog/datatables/posts/data/?created_at__gte=2024-01-01&title__contains=tutorial
Sorting
Sort by one or more fields:
GET /api/apps/blog/datatables/posts/data/?ordering=-created_at,title
- Ascending:
ordering=title - Descending:
ordering=-created_at(prefix with -) - Multiple:
ordering=-created_at,title
Populate Relationships
Load related data in a single query:
GET /api/apps/blog/datatables/posts/data/?populate=author,comments
Response with populated relationships:
{
"data": [
{
"id": 1,
"title": "My Post",
"author_id": 5,
"author": {
"id": 5,
"name": "Alice",
"email": "alice@example.com"
},
"comments": [
{"id": 1, "content": "Great!", "author": "Bob"},
{"id": 2, "content": "Thanks", "author": "Carol"}
]
}
]
}
Populate all relationships:
GET /api/apps/blog/datatables/posts/data/?populate=*
See Relationships Guide for advanced population.
Response Formats
Flat Format (default):
GET /api/apps/blog/datatables/posts/data/
Tree Format (hierarchical data):
GET /api/apps/blog/datatables/categories/data/?format=tree
Graph Format (nodes and edges):
GET /api/apps/blog/datatables/users/data/?format=graph&include=descendants
See Hierarchy Guide for tree/graph formats.
Updating Data
Update Single Record
Partially update a record (only specified fields).
- REST API
- Python
- JavaScript
PATCH /api/apps/blog/datatables/posts/data/42/
Content-Type: application/json
{
"status": "published",
"published_at": "2024-01-15T12:00:00Z"
}
Response (200 OK):
{
"data": {
"id": 42,
"title": "My First Post",
"content": "This is the content",
"status": "published",
"published_at": "2024-01-15T12:00:00Z",
"updated_at": "2024-01-15T12:00:00Z"
}
}
# Update a single record (partial update)
updated_post = auth_client.database.update(
"posts",
record_id=42,
data={
"status": "published",
"published_at": "2024-01-15T12:00:00Z"
}
)
print(f"Updated post {updated_post['id']}")
print(f"New status: {updated_post['status']}")
print(f"Published at: {updated_post['published_at']}")
import { Database } from '@taruvi/sdk'
const database = new Database(client)
// Update a single record (partial update)
const updatedPost = await database.from("posts").update(42, {
status: "published",
published_at: "2024-01-15T12:00:00Z"
}).execute()
console.log(`Updated post ${updatedPost.data.id}`)
console.log(`New status: ${updatedPost.data.status}`)
console.log(`Published at: ${updatedPost.data.published_at}`)
Bulk Update
Update multiple records at once. Each object must include id:
- REST API
- Python
- JavaScript
PATCH /api/apps/blog/datatables/posts/data/
Content-Type: application/json
[
{
"id": 42,
"status": "published"
},
{
"id": 43,
"status": "archived"
}
]
Response (200 OK):
{
"data": [
{"id": 42, "status": "published", "updated_at": "2024-01-15T12:00:00Z"},
{"id": 43, "status": "archived", "updated_at": "2024-01-15T12:00:00Z"}
]
}
# Bulk update posts
posts = auth_client.database.update('posts', [
{'id': 42, 'status': 'published'},
{'id': 43, 'status': 'archived'}
])
print(f"Updated {len(posts['data'])} posts")
for post in posts['data']:
print(f"Post {post['id']}: {post['status']}")
// Bulk update posts
const posts = await client.database
.from('posts')
.update([
{ id: 42, status: 'published' },
{ id: 43, status: 'archived' }
])
.execute()
console.log(`Updated ${posts.data.length} posts`)
posts.data.forEach(post => {
console.log(`Post ${post.id}: ${post.status}`)
})
Validation on Update
Updates are validated against the schema:
PATCH /api/apps/blog/datatables/posts/data/42/
Content-Type: application/json
{
"status": "invalid_status"
}
Response (400 Bad Request):
{
"error": "Validation failed",
"details": {
"status": ["Value must be one of: draft, published, archived"]
}
}
Deleting Data
Delete Single Record
- REST API
- Python
- JavaScript
DELETE /api/apps/blog/datatables/posts/data/42/
Response (204 No Content)
# Delete a single record
auth_client.database.delete("posts", record_id=42)
print("Post deleted successfully")
import { Database } from '@taruvi/sdk'
const database = new Database(client)
// Delete a single record
await database.from("posts").delete(42).execute()
console.log("Post deleted successfully")
Bulk Delete by IDs
Delete multiple records by specifying IDs:
DELETE /api/apps/blog/datatables/posts/data/?ids=42,43,44
Response (200 OK):
{
"deleted": 3,
"ids": [42, 43, 44]
}
Bulk Delete by Filter
Delete all records matching a filter:
DELETE /api/apps/blog/datatables/posts/data/?filter={"status":"draft","created_at__lt":"2023-01-01"}
Response (200 OK):
{
"deleted": 15
}
Soft Delete
If your table has an is_deleted field, deletions are soft by default:
- REST API
- Python
- JavaScript
DELETE /api/apps/blog/datatables/posts/data/42/
The record is marked as deleted (is_deleted=true) but remains in the database.
Querying with soft delete:
- By default, soft-deleted records are excluded from queries
- Include deleted:
GET /data/?include_deleted=true - Only deleted:
GET /data/?only_deleted=true
# Soft delete a post (if table has is_deleted field)
auth_client.database.delete('posts', 42)
print("Post soft deleted")
# Query excluding soft-deleted records (default behavior)
active_posts = auth_client.database.query('posts').get()
# Include soft-deleted records
# Note: SDK doesn't directly support include_deleted parameter
# Workaround: Use filter to explicitly include deleted records
all_posts = auth_client.database.query('posts') \
.filter('is_deleted', 'in', [True, False]) \
.get()
# Only soft-deleted records
deleted_posts = auth_client.database.query('posts') \
.filter('is_deleted', 'eq', True) \
.get()
SDK Note: The include_deleted and only_deleted query parameters are REST-only. Use explicit filters on is_deleted field as shown above.
// Soft delete a post (if table has is_deleted field)
await client.database
.from('posts')
.delete('42')
.execute()
console.log("Post soft deleted")
// Query excluding soft-deleted records (default behavior)
const activePosts = await client.database
.from('posts')
.execute()
// Include soft-deleted records
// Note: SDK doesn't directly support include_deleted parameter
// Workaround: Use filter to explicitly include deleted records
const allPosts = await client.database
.from('posts')
.filter('is_deleted', 'in', [true, false])
.execute()
// Only soft-deleted records
const deletedPosts = await client.database
.from('posts')
.filter('is_deleted', 'eq', true)
.execute()
SDK Note: The include_deleted and only_deleted query parameters are REST-only. Use explicit filters on is_deleted field as shown above.
Hard Delete
Permanently remove records:
- REST API
- Python
- JavaScript
DELETE /api/apps/blog/datatables/posts/data/42/?hard_delete=true
# Hard delete not directly supported in SDK
# Workaround: Use REST API directly or manually update is_deleted field first
# Option 1: Use HTTP client to call REST endpoint
import requests
response = requests.delete(
'http://api.example.com/api/apps/blog/datatables/posts/data/42/?hard_delete=true',
headers={'Authorization': f'Bearer {auth_client.token}'}
)
# Option 2: If hard delete is table default behavior (no is_deleted field)
# Regular delete will permanently remove the record
auth_client.database.delete('posts', 42)
SDK Limitation: The hard_delete parameter is REST-only. For tables without is_deleted field, regular delete is permanent.
// Hard delete not directly supported in SDK
// Workaround: Use REST API directly or manually update is_deleted field first
// Option 1: Use fetch to call REST endpoint
await fetch(
'http://api.example.com/api/apps/blog/datatables/posts/data/42/?hard_delete=true',
{
method: 'DELETE',
headers: { 'Authorization': `Bearer ${client.token}` }
}
)
// Option 2: If hard delete is table default behavior (no is_deleted field)
// Regular delete will permanently remove the record
await client.database
.from('posts')
.delete('42')
.execute()
SDK Limitation: The hard_delete parameter is REST-only. For tables without is_deleted field, regular delete is permanent.
Cascade Behavior
Foreign key cascade rules are respected:
- CASCADE: Related records are deleted
- SET NULL: Foreign keys are set to NULL
- RESTRICT: Delete fails if related records exist
Example (403 Forbidden):
{
"error": "Cannot delete record",
"detail": "Post has 5 related comments. Delete comments first or use CASCADE."
}
Error Handling
Common HTTP Status Codes
| Code | Meaning | Example |
|---|---|---|
| 200 | Success | Record updated |
| 201 | Created | New record created |
| 204 | No Content | Record deleted |
| 400 | Bad Request | Validation failed |
| 404 | Not Found | Record doesn't exist |
| 409 | Conflict | Unique constraint violation |
| 422 | Unprocessable Entity | Business logic error |
Validation Errors (400)
{
"error": "Validation failed",
"details": {
"email": ["This field is required"],
"age": ["Value must be >= 0"]
}
}
Unique Constraint Violations (409)
{
"error": "Unique constraint violated",
"detail": "Record with email='alice@example.com' already exists",
"field": "email"
}
Foreign Key Violations (400)
{
"error": "Foreign key constraint violated",
"detail": "Author with id=999 does not exist",
"field": "author_id"
}
Best Practices
1. Use Bulk Operations for Multiple Records
❌ Don't:
// Inefficient: 100 separate requests
for (const user of users) {
await fetch('/data/', {
method: 'POST',
body: JSON.stringify(user)
});
}
✅ Do:
// Efficient: Single bulk request
await fetch('/data/', {
method: 'POST',
body: JSON.stringify(users) // Array of objects
});
2. Use Upsert for Idempotency
When you're unsure if a record exists, use upsert:
// Idempotent operation
await fetch('/data/upsert/?unique_fields=email', {
method: 'POST',
body: JSON.stringify({
email: 'alice@example.com',
name: 'Alice'
})
});
3. Populate Only Needed Relationships
❌ Don't:
GET /data/?populate=* # Loads ALL relationships (slow!)
✅ Do:
GET /data/?populate=author # Only what you need
4. Paginate Large Result Sets
Always use pagination for tables with many records:
GET /data/?page_size=50&page=1
5. Use Partial Updates
// Only send changed fields
await fetch('/data/42/', {
method: 'PATCH',
body: JSON.stringify({ status: 'published' }) // Not the entire record
});
6. Handle Errors Gracefully
try {
const response = await fetch('/data/', {
method: 'POST',
body: JSON.stringify(newRecord)
});
if (!response.ok) {
const error = await response.json();
if (response.status === 400) {
// Handle validation errors
console.error('Validation errors:', error.details);
} else if (response.status === 409) {
// Handle unique constraint violation
console.error('Duplicate record:', error.detail);
}
}
const data = await response.json();
return data;
} catch (error) {
console.error('Network error:', error);
}
7. Leverage Transactions (Single vs Bulk)
- Single operations: Atomic by default
- Bulk operations: All-or-nothing transaction
- If any record fails validation, entire bulk operation is rolled back
Examples
Blog Post Creation
// Create a new blog post
const newPost = await fetch('/api/apps/blog/datatables/posts/data/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({
title: 'Getting Started with Taruvi',
content: 'Taruvi is a powerful backend platform...',
author_id: 5,
category: 'tutorial',
status: 'draft',
tags: ['taruvi', 'tutorial', 'backend']
})
}).then(r => r.json());
console.log('Created post:', newPost.data);
User Profile Update
import requests
# Update user profile
response = requests.patch(
'https://api.example.com/api/apps/myapp/datatables/users/data/42/',
headers={'Authorization': 'Bearer YOUR_TOKEN'},
json={
'bio': 'Software Engineer at Acme Corp',
'location': 'San Francisco, CA',
'website': 'https://example.com'
}
)
if response.status_code == 200:
user = response.json()['data']
print(f"Updated user: {user['name']}")
Bulk Import CSV Data
// Convert CSV to JSON and bulk insert
const csvData = `name,email,age
Alice,alice@example.com,30
Bob,bob@example.com,25
Carol,carol@example.com,35`;
const users = csvData.split('\n').slice(1).map(line => {
const [name, email, age] = line.split(',');
return { name, email, age: parseInt(age) };
});
const response = await fetch('/api/apps/myapp/datatables/users/data/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify(users)
});
const result = await response.json();
console.log(`Imported ${result.data.length} users`);
Archive Old Records
import requests
from datetime import datetime, timedelta
# Archive posts older than 1 year
one_year_ago = (datetime.now() - timedelta(days=365)).isoformat()
response = requests.patch(
'https://api.example.com/api/apps/blog/datatables/posts/data/',
headers={'Authorization': 'Bearer YOUR_TOKEN'},
params={
'created_at__lt': one_year_ago,
'status': 'published'
},
json={'status': 'archived'}
)
print(f"Archived {response.json()['total']} posts")
Related Documentation
- Querying Guide - Complete filter operators and query syntax
- Aggregations - GROUP BY and aggregate functions
- Relationships - Working with foreign keys and populate
- Schema Management - Defining table structures
- API Endpoints Reference - Complete endpoint listing